/* * Copyright ThinkTank Maths Limited 2006 - 2008 * * This file is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * This file is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this file. If not, see <http://www.gnu.org/licenses/>. */ package com.openlapi; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.Vector; import thinktank.j2me.TTUtils; /** * This is the starting point for applications using this API and represents a source of * the location information. A LocationProvider represents a location-providing module, * generating Locations. * <p> * Applications obtain LocationProvider instances (classes implementing the actual * functionality by extending this abstract class) by calling the factory method. It is * the responsibility of the implementation to return the correct LocationProvider-derived * object. * <p> * Applications that need to specify criteria for the location provider selection, must * first create a Criteria object, and pass it to the factory method. The methods that * access the location related information shall throw SecurityException if the * application does not have the relevant permission to access the location information. */ public abstract class LocationProvider { /** * Availability status code: the location provider is available. */ public static final int AVAILABLE = 1; /** * Availability status code: the location provider is out of service. Being out of * service means that the method is unavailable and the implementation is not able to * expect that this situation would change in the near future. An example is when * using a location method implemented in an external device and the external device * is detached. */ public static final int OUT_OF_SERVICE = 3; /** * Availability status code: the location provider is temporarily unavailable. * Temporary unavailability means that the method is unavailable due to reasons that * can be expected to possibly change in the future and the provider to become * available. An example is not being able to receive the signal because the signal * used by the location method is currently being obstructed, e.g. when deep inside a * building for satellite based methods. However, a very short transient obstruction * of the signal should not cause the provider to toggle quickly between * TEMPORARILY_UNAVAILABLE and AVAILABLE. */ public static final int TEMPORARILY_UNAVAILABLE = 2; // hold the latest valid location here so that it may be requested without expense. private static volatile Location lastKnownLocation = Location.getInvalid(); /** * This is where we store the proximity listeners. The elements are 3 element Object [] * arrays:- * <ul> * <li>[0] are the ProximityListeners</li> * <li>[1] are the Coordinates</li> * <li>[2] is the proximityRadius as a Float</li> * </ul> */ private static final Vector listeners = new Vector(); /** * Adds a ProximityListener for updates when proximity to the specified coordinates is * detected. If this method is called with a ProximityListener that is already * registered, the registration to the specified coordinates is added in addition to * the set of coordinates it has been previously registered for. A single listener can * handle events for multiple sets of coordinates. * <p> * If the current location is known to be within the proximity radius of the specified * coordinates, the listener shall be called immediately. * <p> * Detecting the proximity to the defined coordinates is done on a best effort basis * by the implementation. Due to the limitations of the methods used to implement * this, there are no guarantees that the proximity is always detected; especially in * situations where the terminal briefly enters the proximity area and exits it * shortly afterwards, it is possible that the implementation misses this. It is * optional to provide this feature as it may not be reasonably implementable with all * methods used to implement this API. * <p> * If the implementation is capable of supporting the proximity monitoring and has * resources to add the new listener and coordinates to be monitored but the * monitoring can't be currently done due to the current state of the method used to * implement it, this method shall succeed and the monitoringStateChanged method of * the listener shall be immediately called to notify that the monitoring is not * active currently. * * @param listener * the listener to be registered * @param coordinates * the coordinates to be registered * @param proximityRadius * the radius in meters that is considered to be the threshold for being in * the proximity of the specified coordinates * @throws LocationException * if the platform does not have resources to add a new listener and * coordinates to be monitored or does not support proximity monitoring at * all * @throws IllegalArgumentException * if the proximity radius is 0 or negative* or Float.NaN * @throws NullPointerException * if the listener or coordinates parameter is null * @throws SecurityException * if the application does not have the permission to register a proximity * listener */ public static void addProximityListener(ProximityListener listener, Coordinates coordinates, float proximityRadius) throws LocationException, IllegalArgumentException, NullPointerException, SecurityException { // test Security permissions OpenLAPICommon.testPermission( "javax.microedition.location.ProximityListener"); if ((listener == null) || (coordinates == null)) throw new NullPointerException(); if ((proximityRadius <= 0.0) || (Float.isNaN(proximityRadius))) throw new IllegalArgumentException(); if (listeners == null) // impossible, but keeps Eclipse quiet about never thrown exceptions throw new LocationException(); Float radius = new Float(proximityRadius); Object[] listenerArray = new Object[3]; listenerArray[0] = listener; listenerArray[1] = coordinates; listenerArray[2] = radius; listeners.addElement(listenerArray); } /** * This factory method is used to get an actual LocationProvider implementation based * on the defined criteria. The implementation chooses the LocationProvider so that it * best fits the defined criteria, taking into account also possible implementation * dependent preferences of the end user. If no concrete LocationProvider could be * created that typically can match the defined criteria but there are other location * providers not meeting the criteria that could be returned for a more relaxed * criteria, null is returned to indicate this. The LocationException is thrown, if * all supported location providers are out of service. A LocationProvider instance is * returned if there is a location provider meeting the criteria in either the * available or temporarily unavailable state. Implementations should try to select * providers in the available state before providers in temporarily unavailable state, * but this can't be always guaranteed because the implementation may not always know * the state correctly at this point in time. If a LocationProvider meeting the * criteria can be supported but is currently out of service, it shall not be * returned. * <p> * When this method is called with a Criteria that has all fields set to the default * values (i.e. the least restrictive criteria possible), the implementation shall * return a LocationProvider if there is any provider that isn't in the out of service * state. Passing null as the parameter is equal to passing a Criteria that has all * fields set to the default values, i.e. the least restrictive set of criteria. * <p> * This method only makes the selection of the provider based on the criteria and is * intended to return it quickly to the application. Any possible initialization of * the provider is done at an implementation dependent time and MUST NOT block the * call to this method. * <p> * This method may, depending on the implementation, return the same LocationProvider * instance as has been returned previously from this method to the calling * application, if the same instance can be used to fulfill both defined criteria. Note * that there can be only one LocationListener associated with a LocationProvider * instance. * * @param criteria * the criteria for provider selection or null to indicate the least * restrictive criteria with default values * @return a LocationProvider meeting the defined criteria or null if a * LocationProvider that meets the defined criteria can't be returned but * there are other supported available or temporarily unavailable providers * that do not meet the criteria. * @throws LocationException * if all LocationProviders are currently out of service */ public static LocationProvider getInstance(Criteria criteria) throws LocationException { String mode = null; try { mode = getMode(); } catch (IOException e) { TTUtils.log(e.getMessage()); } //We'll default to BUG mode, which will try to access the first OSGi INMEARawFeed available in the Service Registry. if (mode == null || "BUG".equals(mode)) { return new LocationProviderBUG(criteria); } else if ("NMEA".equals(mode)) { return new LocationProviderNMEA(criteria, "/tmp/nmea.log"); } else if ("LMS".equals(mode)) { // random entry from the LandmarkStore return new LocationProviderLMS(criteria, null); } try { // name not recognised, allow reflection to have a go // this allows people to create their own implementations // outside of OpenLAPI Class implClass = Class.forName(mode); return (LocationProvider) implClass.newInstance(); } catch (Throwable ex) { throw new LocationException("No such LocationProvider: " + mode + " " + ex.getMessage()); } } /** * Returns the last known location that the implementation has. This is the best * estimate that the implementation has for the previously known location. * Applications can use this method to obtain the last known location and check the * timestamp and other fields to determine if this is recent enough and good enough * for the application to use without needing to make a new request for the current * location. * * @return a location object. null is returned if the implementation doesn't have any * previous location information. * @throws SecurityException * if the calling application does not have a permission to query the * location information */ public static Location getLastKnownLocation() throws SecurityException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.Location"); return lastKnownLocation; } /** * Removes a ProximityListener from the list of recipients for updates. If the * specified listener is not registered or if the parameter is null, this method * silently returns with no action. * <p> * <i>NOTE: the specification conflicts with itself on how to handle null parameter. * We throw the exception.</i> * * @param listener * the listener to remove * @throws NullPointerException * if the parameter is null */ public static void removeProximityListener(ProximityListener listener) throws NullPointerException { if (listener == null) throw new NullPointerException(); // synchronise because this is temporarily breaking the Enumeration synchronized (listeners) { // can't use Enumeration because we are removing elements Object[] list = new Object[listeners.size()]; Enumeration en = listeners.elements(); for (int i = 0; en.hasMoreElements(); i++) { list[i] = en.nextElement(); } for (int i = 0; i < list.length; i++) { Object[] listenerArray = (Object[]) list[i]; // remove every registration of this listener if (listenerArray[0].equals(listener)) { listeners.removeElement(listenerArray); } } } } /** * Check if the ProximityListeners and alert them if the given Location meets their * criteria. * <p> * Note that this involves calculating the distance between the current location and * the location the listener is watching. The Coordinates.distance() method is a very * expensive operation, so we check bounding boxes on the latitude and longitude to * see if the location is in the general region of the listener. This effectively * enables the use of many listeners with relatively low numerical cost. * * @param location */ private static void alertProximityListeners(Location location) { // Syncronised incase we receive another update while calling this which // may result in the Enumeration breaking synchronized (listeners) { // alert proximity listeners QualifiedCoordinates current = location.getQualifiedCoordinates(); Enumeration en = listeners.elements(); for (; en.hasMoreElements();) { Object[] listenerArray = (Object[]) en.nextElement(); Coordinates watching = (Coordinates) listenerArray[1]; float radius = ((Float) listenerArray[2]).floatValue(); float accuracy = current.getHorizontalAccuracy(); if (!Float.isNaN(accuracy)) { // if there was an accuracy associated to our current // Location, consider it in the radius radius += accuracy; } // simple bounding on latitude is much less costly than distance // we use semi-minor circumference to get max bounding box double radiusAsAngle = radius * OpenLAPICommon.INV_MINOR_CIRCUMFERENCE; double latDiff = Math.abs(watching.getLatitude() - current.getLatitude()); if (latDiff > radiusAsAngle) { // outside bounding box continue; } // The equivalent for longitude is more complicated double longDiff = longMinInMeters(watching, current); if (longDiff > radius) { // outside bounding box continue; } // calculating actual distance is very costly float distance = watching.distance(current); if (distance <= radius) { // within the tolerance // remove the listener from the list listeners.removeElement(listenerArray); // alert the listener ProximityListener listener = (ProximityListener) listenerArray[0]; listener.proximityEvent(watching, location); } } } } /** * Mechanism for setting the emulator mode by setting a property in an application * resource file. * <p> * Because the J2ME specs made the user properties part of the MIDlet class, and not * static, we have to look in the manifest file directly. gotta love those J2ME * designers... * * @return * @throws IOException */ private static String getMode() throws IOException { // if we got here, the variable wasn't defined in the jar file InputStream in = LocationProvider.class.getResourceAsStream("/tmp/OpenLAPI-mode.txt"); if (in != null) { try { String line = TTUtils.readLine(in); return line; } finally { in.close(); } } //Try to load a system property. if (System.getProperties().get("com.openlapi.mode") != null) { return (String) System.getProperties().get("com.openlapi.mode"); } //Failed, assume default. return null; } /** * When given two Coordinate objects, this method will first calculate the difference * in longitude between them. It will then calculate and return the minimal * longitudinal distance between them in meters, which would be the distance if the * most equatorial Coordinate were moved to be on the same latitude as the other. * * @param c1 * @param c2 * @return */ private static double longMinInMeters(Coordinates c1, Coordinates c2) { // calculate difference in longitude, which may be via the back double front = Math.abs(c1.getLongitude() - c2.getLongitude()); double back = Math.abs(360 - Math.abs(c1.getLongitude()) - Math.abs(c2. getLongitude())); double diffLong = (front < back) ? front : back; // calculate the latitude closer to a pole double lat1 = Math.abs(c1.getLatitude()); double lat2 = Math.abs(c2.getLatitude()); double lat = (lat1 > lat2) ? lat1 : lat2; // calculate the cross sectional radius of the earth for each latitude double radius = Math.abs(OpenLAPICommon.SEMI_MAJOR * Math.cos(Math.toRadians(lat))); // calculate the distance in meters at each latitude double distance = 2 * Math.PI * radius * (diffLong / 360d); return distance; } /** * Convenience method that tells all the proximity listeners that we are periodically * reporting the location or not. * * @param status */ static protected void alertProximityListeners(boolean status) { Enumeration en = listeners.elements(); for (; en.hasMoreElements();) { Object[] listenerArray = (Object[]) en.nextElement(); ProximityListener listener = (ProximityListener) listenerArray[0]; listener.monitoringStateChanged(status); } } /** * This is an implementation specific method to alert the LocationProvider of the * latest location. This also ensures that all ProximityListeners are alerted if they * are in range. * <p> * Will check if the given Location has a more recent timestamp before committing to * the update. * * @param location */ protected static void setLastKnownLocation(Location location) { if (location == null || !location.isValid()) return; // only update if the given Location is actually newer! remember, some // devices provide locations with a time delay. if (location.getTimestamp() <= lastKnownLocation.getTimestamp()) return; lastKnownLocation = location; // alert ProximityListeners alertProximityListeners(location); } /** * Empty constructor to help implementations and extensions. This is not intended to * be used by applications. Applications should not make subclasses of this class and * invoke this constructor from the subclass. */ protected LocationProvider() { } /** * Retrieves a Location with the constraints given by the Criteria associated with * this class. If no result could be retrieved, a LocationException is thrown. If the * location can't be determined within the timeout period specified in the parameter, * the method shall throw a LocationException. If the provider is temporarily * unavailable, the implementation shall wait and try to obtain the location until the * timeout expires. If the provider is out of service, then the LocationException is * thrown immediately. * <p> * Note that the individual Location returned might not fulfil exactly the criteria * used for selecting this LocationProvider. The Criteria is used to select a location * provider that typically is able to meet the defined criteria, but not necessarily * for every individual location measurement. * * @param timeout * a timeout value in seconds. -1 is used to indicate that the * implementation shall use its default timeout value for this provider. * @return * @throws LocationException * if the location couldn't be retrieved or if the timeout period expired * @throws InterruptedException * if the operation is interrupted by calling reset() from another thread * @throws SecurityException * if the calling application does not have a permission to query the * location information * @throws IllegalArgumentException * if the timeout = 0 or timeout < -1 */ public abstract Location getLocation(int timeout) throws LocationException, InterruptedException, SecurityException, IllegalArgumentException; /** * Returns the current state of this LocationProvider. The return value shall be one * of the availability status code constants defined in this class. * * @return the availability state of this LocationProvider */ public abstract int getState(); /** * Resets the LocationProvider. All pending synchronous location requests will be * aborted and any blocked getLocation method calls will terminate with * InterruptedException. * <p> * Applications can use this method e.g. when exiting to have its threads freed from * blocking synchronous operations. */ public abstract void reset(); /** * Adds a LocationListener for updates at the defined interval. The listener will be * called with updated location at the defined interval. The listener also gets * updates when the availablilty state of the LocationProvider changes. Passing in -1 * as the interval selects the default interval which is dependent on the used * location method. Passing in 0 as the interval registers the listener to only * receive provider status updates and not location updates at all. * <p> * Only one listener can be registered with each LocationProvider instance. Setting * the listener replaces any possibly previously set listener. Setting the listener to * null cancels the registration of any previously set listener. * <p> * The implementation shall initiate obtaining the first location result immediately * when the listener is registered and provide the location to the listener as soon as * it is available. Subsequent location updates will happen at the defined interval * after the first one. If the specified update interval is smaller than the time it * takes to obtain the first result, the listener shall receive location updates with * invalid Locations at the defined interval until the first location result is * available. Note that prior to getting the first valid location result, the timeout * parameter has no effect. When the first valid location result is obtained, the * implementation may return it to the application immediately, i.e. before the next * interval is due. This implies that in the beginning when starting to obtain * location results, the listener may first get updates with invalid location results * at the defined interval and when the first valid location result is obtained, it * may be returned to the listener as soon as it is available before the next interval * is due. After the first valid location result is delivered to the application the * timeout parameter is used and the next update is delivered between the time defined * by the interval and time interval+timeout after the previous update. * <p> * The timeout parameter determines a timeout that is used if it's not possible to * obtain a new location result when the update is scheduled to be provided. This * timeout value indicates how many seconds the update is allowed to be provided late * compared to the defined interval. If it's not possible to get a new location result * (interval + timeout) seconds after the previous update, the update will be made and * an invalid Location instance is returned. This is also done if the reason for the * inability to obtain a new location result is due to the provider being temporarily * unavailable or out of service. For example, if the interval is 60 seconds and the * timeout is 10 seconds, the update must be delivered at most 70 seconds after the * previous update and if no new location result is available by that time the update * will be made with an invalid Location instance. * <p> * The maxAge parameter defines how old the location result is allowed to be provided * when the update is made. This allows the implementation to reuse location results * if it has a recent location result when the update is due to be delivered. This * parameter can only be used to indicate a larger value than the normal time of * obtaining a location result by a location method. The normal time of obtaining the * location result means the time it takes normally to obtain the result when a * request is made. If the application specifies a time value that is less than what * can be realized with the used location method, the implementation shall provide as * recent location results as are possible with the used location method. For example, * if the interval is 60 seconds, the maxAge is 20 seconds and normal time to obtain * the result is 10 seconds, the implementation would normally start obtaining the * result 50 seconds after the previous update. If there is a location result * otherwise available that is more recent than 40 seconds after the previous update, * then the maxAge setting to 20 seconds allows to return this result and not start * obtaining a new one. * <p> * The requirements for the intervals hold while the application is executing. If the * application environment or the device is suspended so that the application will not * execute at all and then the environment is later resumed, the periodic updates MUST * continue at the defined interval but there may be a shift after the suspension * period compared to the original interval schedule. * * @param listener * the listener to be registered. If set to null the registration of any * previously set listener is cancelled. * @param interval * the interval in seconds. -1 is used for the default interval of this * provider. 0 is used to indicate that the application wants to receive * only provider status updates and not location updates at all. * @param timeout * timeout value in seconds, must be greater than 0. if the value is -1, * the default timeout for this provider is used. Also, if the interval is * -1 to indicate the default, the value of this parameter has no effect * and the default timeout for this provider is used. If timeout == -1 and * interval > 0 and the default timeout of the provider is greater than the * specified interval, then the timeout parameter is handled as if the * application had passed the same value as timeout as the interval (i.e. * timeout is considered to be equal to the interval). If the interval is * 0, this parameter has no effect. * @param maxAge * maximum age of the returned location in seconds, must be greater than 0 * or equal to -1 to indicate that the default maximum age for this * provider is used. Also, if the interval is -1 to indicate the default, * the value of this parameter has no effect and the default maximum age * for this provider is used. */ public abstract void setLocationListener(LocationListener listener, int interval, int timeout, int maxAge); }