Skip to content

Instantly share code, notes, and snippets.

@markcmarino
Last active February 18, 2020 20:01
Show Gist options
  • Save markcmarino/62becfcd7d8daf3e7beb7568870353d1 to your computer and use it in GitHub Desktop.
Save markcmarino/62becfcd7d8daf3e7beb7568870353d1 to your computer and use it in GitHub Desktop.
Code from the Transborder Immigrant Tool
/* WalkingtoolsGpx: XML, APIs, and Apps for Walking Artists
Copyright (C) 2007-2012 Walkingtoools project/B.A.N.G Lab UCSD
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// note: good idea to extend the dowsing interface to also include (or redirect users)
// to sites that are within 500 meters.
package edu.ucsd.calit2.TransBorderTool;
import edu.ucsd.calit2.TransBorderTool.international.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.location.*;
import java.io.*;
import java.util.*;
import javax.microedition.media.*;
import net.walkingtools.javame.util.AudioArrayPlayer;
/**
* @author Brett Stalbaum and Jason Najarro
* @version 0.5.5
*/
public class TBMIDlet extends MIDlet implements DowsingCompassListener, CommandListener {
private Display display = null;
// current displayable will normally be the tbDowsingCompass, but if expired, an alert.
private DowsingCompass tbDowsingCompass = null;
private Vector nearbyWPList = null;
private static final int SEARCH_DISTANCE = 10000; // 10K
private List targetList = null;
private Alert arrivedAlert = null;
private Alert waypointAheadAlert = null;
private Alert expired = null;
private boolean isExpired = false;
private Alert expirationWarning = null;
private boolean expireWarning = false;
private Alert startUpDisplay = null;
private boolean startUpAlert = false;
private Alert noNearbyWaypoints = null;
private Alert minimalistInfoAlert = null;
private Command exit = null;
private Command ignore = null;
private Command cancel = null;
private Command listNearbyWPs = null;
private Command setTargetManual = null;
private Command setTargetAuto = null;
private AudioTimer audioTimer = null;
// private String URL = "http://internetjunkee.com/transborder/GPScourseFinal.gpx";
private LocationProvider lp = null;
private TBCoordinates aheadCoords = null;
// using only one audio player for two kinds of sound. The first is the
// poems, the second is the Audio UI elements
private static final String[] audioStrings = {
"1-GAIN_04-02.wav", "2-GAIN_02-03.wav", "2-GAIN_03-01.wav",
"3-GAIN_01.wav", "3-GAIN_02-02.wav", "5-GAIN_02-07.wav",// 6 poems
"arriving.wav", "expiration.wav", "expired.wav", "found.wav", // AudioUI
"lowgps.wav", "move.wav", "nosites.wav", "pointing.wav", // AudioUI
"read.wav", "searching.wav", "startup.wav", "beep.wav" // AudioUI
};
private static final int NUMBER_OF_POEMS = 6;
private boolean running = false;
private boolean navigating = false;
// if the MIDlet is getting an update
// interval which is adequate for
// dymanic navigation, dynamicNavigation should be true
private boolean dynamicNavigation = false;
private Ticker minimalistTicker = null;
private net.walkingtools.international.Translation translation = null;
private AudioArrayPlayer audioPlayer = null;
private byte moveWarningEnervator = 1;
/**
* Constructor for a TransBorderMIDlet
*/
public TBMIDlet() {
// load the translation
translation = Translation.loadTranslation(getAppProperty("language"));
// get the display
if (display == null) {
display = Display.getDisplay(this);
}
// test value for jad file... can delete
//System.out.println(System.currentTimeMillis()+1000*60*60*24*8);
// set up the test alert first (for debugging on phone)
exit = new Command(translation.translate("Exit"), Command.EXIT, 0);
// get the gpx file
String gpxFile = this.getAppProperty("GPXFile");
tbDowsingCompass = new DowsingCompass(gpxFile);
int width = tbDowsingCompass.getWidth();
Image errorImage = null;
Image tbImage = null;
if (width < 150) {
errorImage = loadImage("error_sm.png");
tbImage = loadImage("tb_sm.png");
} else {
errorImage = loadImage("error.png");
tbImage = loadImage("tb.png");
}
// first, validate the expiration value
ignore = new Command(translation.translate("Ignore"), Command.CANCEL, 0);
String expirationDate = this.getAppProperty("Expiration-Date");
long exp = Long.parseLong(expirationDate);
if (exp <= System.currentTimeMillis()) {
expired = new Alert(translation.translate("Data expired"),
translation.translate("The data is expired, TBTool is not safe to use."),
errorImage,
AlertType.ERROR);
expired.setTimeout(Alert.FOREVER);
expired.addCommand(exit);
expired.addCommand(ignore);
expired.setCommandListener(this);
isExpired = true;
} else if (exp <= System.currentTimeMillis() + (1000 * 60 * 60 * 24 * 7) ) { // 7 day warning
Date date = new Date(exp);
expirationWarning = new Alert(translation.translate("Expiration Warning"),
translation.translate("\nThe data will expire on:\n") + date.toString() +
translation.translate("\nTrans Border Immigrant Saftey Tool\nEDT/BANGLAB/CRCA/CALIT2/VISARTS/UCSD\n\n"),
errorImage,
AlertType.WARNING);
expirationWarning.addCommand(ignore);
expirationWarning.setTimeout(15000);
expirationWarning.setCommandListener(this);
expireWarning = true;
} else {
Date date = new Date(exp);
startUpDisplay = new Alert(translation.translate("Trans Border Immigrant Tool"),
translation.translate("\nExpires: ") + date.toString() +
translation.translate("\nTrans Border Immigrant Saftey Tool\nEDT/BANGLAB/CRCA/CALIT2/VISARTS/UCSD\n\n"),
tbImage,
AlertType.INFO);
startUpDisplay.addCommand(ignore);
startUpDisplay.setTimeout(10000);
startUpDisplay.setCommandListener(this);
startUpAlert = true;
}
noNearbyWaypoints = new Alert(translation.translate("No Nearby Points"),
translation.translate("There are no sites within ") +
(int)((SEARCH_DISTANCE / 1000.0) + .5)
+ translation.translate(" Kilometers."),
tbImage,
AlertType.WARNING);
noNearbyWaypoints.addCommand(ignore);
noNearbyWaypoints.setTimeout(10000);
noNearbyWaypoints.setCommandListener(this);
minimalistInfoAlert = new Alert(translation.translate("Minimal Mode"),
"",
tbImage,
AlertType.WARNING);
minimalistInfoAlert.addCommand(ignore);
minimalistInfoAlert.setTimeout(Alert.FOREVER);
minimalistInfoAlert.setCommandListener(this);
nearbyWPList = new Vector();
cancel = new Command(translation.translate("Cancel"), Command.CANCEL, 0);
listNearbyWPs = new Command(translation.translate("Find"), Command.SCREEN, 1);
setTargetManual = new Command(translation.translate("Select"), Command.SCREEN, 1);
setTargetAuto = new Command(translation.translate("Set Target"), Command.SCREEN, 1);
/* through a lot of tedious testing, I discovered that these
constructors of the TextFields were throwing an IllegalArgumentException
when using TextField.DECIMAL or TextField.NUMERIC constraints. The
following is from the javadoc. It seems not to contradict the
use of TextField.DECIMAL or NUMERIC given that I was setting
the forms to a decimal/numeric value... hmmmmm... this must be an
issue in in iden implementation.
"Some constraints, such as DECIMAL, require the implementation to
perform syntactic validation of the contents of the text object.
The syntax checking is performed on the actual contents of the text
object, which may differ from the displayed contents as described
above. Syntax checking is performed on the initial contents passed
to the constructors, and it is also enforced for all method calls
that affect the contents of the text object. The methods and
constructors throw IllegalArgumentException if they would result
in the contents of the text object not conforming to the required
syntax."
*/
tbDowsingCompass.addCommand(exit);
tbDowsingCompass.addCommand(listNearbyWPs);
tbDowsingCompass.setCommandListener(this);
tbDowsingCompass.addNavigatorListener(this);
targetList = new List(translation.translate("Select a Target"), List.IMPLICIT);
targetList.addCommand(cancel);
targetList.addCommand(setTargetManual);
targetList.setCommandListener(this);
waypointAheadAlert = new Alert(translation.translate("Site Ahead!"),
translation.translate("Site Ahead!"),
tbImage, AlertType.INFO);
waypointAheadAlert.setTimeout(Alert.FOREVER);
waypointAheadAlert.addCommand(ignore);
waypointAheadAlert.addCommand(setTargetAuto);
waypointAheadAlert.setCommandListener(this);
arrivedAlert = new Alert(translation.translate("Arrived at Site"),
translation.translate("Arrived at Site"),
tbImage, AlertType.INFO);
arrivedAlert.setTimeout(Alert.FOREVER);
arrivedAlert.addCommand(ignore);
arrivedAlert.setCommandListener(this);
minimalistTicker = new Ticker(
translation.translate("Minimal or no GPS signal. Alert will give direction and distance information if possible.")
);
dynamicNavigation = true; // assume active navigation at startup of gps to give it a chance to fix
// set up location provider
// Set criteria for selecting a location provider:
// accurate to 50 meters horizontally
try {
Criteria cr = new Criteria();
cr.setHorizontalAccuracy(50);
// we can set other criteria that we require
cr.setSpeedAndCourseRequired(true);
cr.setPreferredResponseTime(2000);
cr.setAltitudeRequired(true);
try {
// Get an instance of the provider
lp = LocationProvider.getInstance(cr);
} catch (LocationException e) { // if this happens, lp could not get a location
display.setCurrent(new Alert(translation.translate("Exception on getting location provider"),
translation.translate("Exception on getting location provider") + ':' + e.toString(),
null,
AlertType.INFO));
}
// register this with the location listener
// the second argument is the interval. -1 is a flag that says, "whatever works best for you"
// the third arg is the timeout, or, how many seconds past the interval defined in arg 2
// the provider should wait before it returns and invalid Location
// the fourth is the maxAge of a valid location. The provider may provide a valid location
// in lieu of a current location as long as it is not older than this.
lp.setLocationListener(tbDowsingCompass, 2, 2, 2);
} catch (SecurityException se) {
Alert noLocationService = new Alert(translation.translate("TBtool requires location"),
translation.translate("The Transborder Immigrant Tool needs access to location services.") +
translation.translate("Try answering \"Yes\" on startup to grant TBtoool access."),
errorImage,
AlertType.INFO);
noLocationService.setTimeout(Alert.FOREVER);
noLocationService.addCommand(exit);
noLocationService.setCommandListener(this);
display.setCurrent(noLocationService);
}
}
protected void startApp() throws MIDletStateChangeException {
// get the display
if (display == null) {
display = Display.getDisplay(this);
}
// this thread to randomly play audio file
try {
audioPlayer = new AudioArrayPlayer("audio", audioStrings, true); // true, in audio cueing mode
//InputStream in = getClass().getResourceAsStream("/audio/beep.wav");
audioTimer = new AudioTimer();
running = true;
audioTimer.start(); // start audio thread
} catch (IOException e) {
Alert bailOnAudioException = new Alert(translation.translate("Could not load audio"),
translation.translate("Could not load audio"),
loadImage("error_sm.png"),
AlertType.INFO);
bailOnAudioException.setTimeout(Alert.FOREVER);
bailOnAudioException.addCommand(exit);
display.setCurrent(bailOnAudioException);
} catch (MediaException e) {
Alert bailOnAudioException = new Alert(translation.translate("Could not play audio"),
translation.translate("Could not play audio"),
loadImage("error_sm.png"),
AlertType.INFO);
bailOnAudioException.setTimeout(Alert.FOREVER);
bailOnAudioException.addCommand(exit);
display.setCurrent(bailOnAudioException);
}
// make sure the data is not expired
if (isExpired) {
display.setCurrent(expired);
display.vibrate(1000);
playAudioFile("expired.wav", true);
} else if (expireWarning) {
display.setCurrent(expirationWarning, tbDowsingCompass);
display.vibrate(1000);
playAudioFile("expiration.wav", true);
} else if (startUpAlert) { //first time only
startUpAlert = false;
display.setCurrent(startUpDisplay, tbDowsingCompass);
display.vibrate(1000);
playAudioFile("startup.wav", true);
} else { // we are good to go
display.setCurrent(tbDowsingCompass);
}
}
/**
* edu.ucsd.calit2.TransBorderTool.CompassListener interface method
* Called when user is facing a waypoint
* Displays waypointAheadAlert pertaining to type of waypoint
*/
public void witchingEvent(TBCoordinates mc) {
aheadCoords = mc;
if (display.getCurrent().equals(tbDowsingCompass)) {
waypointAheadAlert.setString(tbDowsingCompass.getInfo(mc));
waypointAheadAlert.setImage(aheadCoords.getIcon());
double distance = tbDowsingCompass.distanceTo(mc);
if (distance > SEARCH_DISTANCE) {
display.vibrate(100);
} else if (distance > 1000) {
display.vibrate(300);
} else if (distance > 500) {
display.vibrate(500);
} else if (distance > 100) {
display.vibrate(800);
}
display.setCurrent(waypointAheadAlert);
display.vibrate(1000);
playAudioFile("found.wav", false);
}
}
/**
* NavigatorListener interface method
* Displays alert when user arrives within range of target
*/
public void arrivedAtTarget(int distance) {
navigating = false;
// stop the compass from navigating
tbDowsingCompass.stopNavigation();
display.setCurrent(arrivedAlert);
display.vibrate(1000);
playAudioFile("arriving.wav", false);
}
// all of the UI audio files are played through this method
// the poems are not played through this method,
// see second arg in playFileName below
private void playAudioFile(String name, boolean interrupt) {
try {
audioPlayer.playFileName(name, interrupt); // true will interrupt a poem if playing
} catch (MediaException e) {
try {
audioPlayer.playFileName("beep.wav", true);
} catch (MediaException ex) {
return;
} catch (Exception eb) {
eb.printStackTrace();
}
}
}
/**
* NavigatorListener interface method
* Called to populate nearby waypoint vector
* once the CompassCanvas detects a valid location
* so user may begin "dowsing" for waypoints
* @param ready true for ready to navigate
*/
public void navigationReady(boolean ready) {
if (ready) {
nearbyWPList = tbDowsingCompass.getNearbyWaypoints(SEARCH_DISTANCE);
if (navigating) {
tbDowsingCompass.removeCommand(listNearbyWPs);
tbDowsingCompass.addCommand(cancel);
} else {
tbDowsingCompass.addCommand(listNearbyWPs);
tbDowsingCompass.removeCommand(cancel);
}
} else {
if (!navigating) {
tbDowsingCompass.removeCommand(listNearbyWPs); // can't use
}
}
}
/**
* NavigatorListener interface method tells the midlet the GPS refresh rate
* of the Navigator (DowsingCompass...) If the MIDlet is getting an update
* interval which is adequate for dymanic navigation then dynamic
* (compass based) navigation should be on.
* Otherwise the phone enters into a minimalist mode that can still provide
* an occaisional alert, useful with less capable phones or in place where
* GPS coverage is poor. In these cases the user may still be able to navigate
* with a magnetic compass.
* @param milliseconds reported milliseconds since last update
*/
public void updateInterval(long milliseconds) {
// if the device is without update for 10 minutes, enter minimal mode
if (milliseconds > 1000*60*10) { // signal is not good
if (dynamicNavigation) { // entering non dynamic mode from dynamic
tbDowsingCompass.setTicker(minimalistTicker);
if (!navigating) {
tbDowsingCompass.removeCommand(listNearbyWPs); // can't use
}
display.vibrate(1000);
playAudioFile("lowgps.wav", true);
}
dynamicNavigation = false;
} else { // we have a good signal
if (!dynamicNavigation) { // we are now returning from a bad signal
// because dN is set to true in the constructor
// we must be returning from non-dynamic to dynamic, not just starting
// restore interface to last state
tbDowsingCompass.setTicker(null);
// Offer any available help to user
// get closest point data into alert string if available
nearbyWPList = tbDowsingCompass.getNearbyWaypoints(SEARCH_DISTANCE);
if (nearbyWPList != null && !nearbyWPList.isEmpty()) {
TBCoordinates target = (TBCoordinates)nearbyWPList.elementAt(0);
Coordinates current = tbDowsingCompass.getCurrentCoords();
float distance = current.distance(target);
String distanceStr = null;
if (distance >= 1000) {
distanceStr = (int)(distance/1000) + translation.translate(" Kilometers");
} else {
distanceStr = distance + translation.translate(" Meters");
}
// create minimalist info alert (if it is just an itermittant single report
// then at least this info will be left on screen as the system goes
// back into non dynamic navigation mode.
minimalistInfoAlert.setString(
translation.translate("Nearest Site: Distance ") + distanceStr + ", " +
translation.translate("Azimuth ") + (int)current.azimuthTo(target)
+ translation.translate(" degrees, General Direction ") +
tbDowsingCompass.directionTo(target)
);
display.setCurrent(minimalistInfoAlert, tbDowsingCompass);
display.vibrate(1000);
playAudioFile("read.wav", false);
} else {
display.setCurrent(noNearbyWaypoints);
display.vibrate(1000);
playAudioFile("nosites.wav", false);
}
}
dynamicNavigation = true;
}
}
/** (non-Javadoc)
* @param arg0
* @throws MIDletStateChangeException
* @see javax.microedition.midlet.MIDlet#destroyApp(boolean)
*/
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
/**
*
*/
protected void pauseApp() {
// TODO Auto-generated method stub
}
private Image loadImage(String str) {
Image image = null;
try {
image = Image.createImage("/img/" + str);
} catch (IOException e) {
image = null;
}
//System.out.println(image);
return image;
}
public void commandAction(Command c, Displayable d) {
if (c == exit) { // exit
running = false;
notifyDestroyed();
} else if (c == cancel) { // stop navigation and reset softkey commands
navigating = false;
tbDowsingCompass.stopNavigation();
tbDowsingCompass.removeCommand(cancel);
if (dynamicNavigation) {
tbDowsingCompass.addCommand(listNearbyWPs);
}
display.setCurrent(tbDowsingCompass);
} else if (c == ignore) { // Returns to compass interface if user chooses not to
// set a dowsingEvent as a target
if (navigating) {
tbDowsingCompass.removeCommand(listNearbyWPs);
tbDowsingCompass.addCommand(cancel);
} else {
tbDowsingCompass.removeCommand(cancel);
if (dynamicNavigation) {
tbDowsingCompass.addCommand(listNearbyWPs);
}
}
display.setCurrent(tbDowsingCompass);
} else if (c == listNearbyWPs) { // Display a List of waypoints within range
// from which user can manually choose a target
// Update nearby waypoint vector
nearbyWPList = tbDowsingCompass.getNearbyWaypoints(SEARCH_DISTANCE);
if (nearbyWPList != null && !nearbyWPList.isEmpty()) {
targetList.deleteAll();
// Loop through waypoint vector adding waypoint
// image and information to list
for (int i = 0; i < nearbyWPList.size(); i++) {
TBCoordinates mc = (TBCoordinates) nearbyWPList.elementAt(i);
targetList.append(tbDowsingCompass.getInfo(mc), mc.getIcon());
}
display.setCurrent(targetList);
} else {
display.setCurrent(noNearbyWaypoints);
playAudioFile("nosites.wav", true);
}
} else if (c == setTargetAuto) { // Set a waypoint detected by a dowsingEvent as the target
navigating = true;
tbDowsingCompass.setTarget(aheadCoords);
// Change commands on tbDowsingCanvas
tbDowsingCompass.removeCommand(listNearbyWPs);
tbDowsingCompass.addCommand(cancel);
display.setCurrent(tbDowsingCompass);
// Set a waypoint selected from nearby waypoint List as the target
} else if (c == setTargetManual) {
navigating = true;
int index = targetList.getSelectedIndex();
tbDowsingCompass.setTarget((TBCoordinates) nearbyWPList.elementAt(index));
//Change Commands on tbDowsingCanvas
tbDowsingCompass.removeCommand(listNearbyWPs);
tbDowsingCompass.addCommand(cancel);
display.setCurrent(tbDowsingCompass);
}
}
public void motionStatusUpdate(boolean isMoving) {
if (isMoving) { // updated to moving
nearbyWPList = tbDowsingCompass.getNearbyWaypoints(SEARCH_DISTANCE); // so update nearby points
} else { // updated not moving
display.vibrate(200);
if (moveWarningEnervator % 5 == 0) { // only play this file ~ every 5th time
playAudioFile("move.wav", false); // the "move for compass" message can be too frequent
}
moveWarningEnervator++;
}
}
// inner class to control audio
class AudioTimer extends Thread {
Random rand = new Random();
public void run() {
while (running) {
try {
Thread.sleep(1000 * 60 * (rand.nextInt(19) + 1)); // sleep random minutes
//Thread.sleep(1000 * 60); // sleep one min (test)
} catch (InterruptedException e) {
running = false;
}
try {
int randIndex = rand.nextInt(NUMBER_OF_POEMS); // poems at the top of the audio array
audioPlayer.play(randIndex, false); // false means to cue the audio if something else is playing
} catch (MediaException ex) {
Alert bailOnAudioException = new Alert(translation.translate("media exception"),
ex.getMessage(),
null,
AlertType.INFO);
bailOnAudioException.setTimeout(Alert.FOREVER);
display.setCurrent(bailOnAudioException);
}
}
}
public void finalize() {
running = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment