核心職責:接收非同步位置更新與接近事件,轉發給對應 UI 元件,並在背景執行緒處理耗時邏輯 (UI 更新、資料庫查詢、警示彈窗)。
package com.nokia.example.location.tourist.model;
import javax.microedition.location.*;
import com.nokia.example.location.tourist.ui.*;
public class TouristData implements LocationListener, ProximityListener {
// ---- UI 參考 (弱耦合,由 UI 自行註冊) ----
private TouristUI touristUI = null;
private CompassUI compassUI = null;
private PitchRollUI pitchRollUI = null;
private LandmarkEditorUI landmarkEditorUI = null;
// ---- Provider 狀態回呼介面 ----
private ProviderStatusListener statusListener = null;
public TouristData(ProviderStatusListener listener) {
this.statusListener = listener;
// 向 ConfigurationProvider 取得已選 Provider 並註冊監聽器
LocationProvider provider = ConfigurationProvider.getInstance().getSelectedProvider();
if (provider != null) {
registerListeners(provider);
}
}
// ---- UI 註冊方法 ----
public void setTouristUI(TouristUI ui) { this.touristUI = ui; }
public void setCompassUI(CompassUI ui) { this.compassUI = ui; }
public void setPitchRollUI(PitchRollUI ui) { this.pitchRollUI = ui; }
public void setLandmarkEditorUI(LandmarkEditorUI ui) { this.landmarkEditorUI = ui; }
// ---- 註冊 LocationListener / ProximityListener ----
private void registerListeners(LocationProvider provider) {
try {
// 間隔 5 秒、無逾時、無最大壽命限制
provider.setLocationListener(this, 5000, 0, 0);
// 針對所有現有地標註冊 ProximityListener (半徑 50 公尺)
Enumeration landmarks = ControlPoints.getInstance().getAllLandmarks();
while (landmarks.hasMoreElements()) {
Landmark lm = (Landmark) landmarks.nextElement();
QualifiedCoordinates qc = lm.getQualifiedCoordinates();
if (qc != null) {
LocationProvider.addProximityListener(this, qc, 50);
}
}
} catch (LocationException e) {
// 資源不足或不支援 Proximity
if (touristUI != null) touristUI.showError("Proximity registration failed");
}
}
// ==================== LocationListener 介面 ====================
public void locationUpdated(final LocationProvider provider, final Location location) {
// 必須極速返回 → 另開執行緒處理
new Thread() {
public void run() {
processLocation(location);
}
}.start();
}
public void providerStateChanged(LocationProvider provider, final int newState) {
// Provider 狀態變更:AVAILABLE(0), TEMPORARILY_UNAVAILABLE(1), OUT_OF_SERVICE(2)
if (statusListener != null) {
statusListener.providerStateChanged(newState);
}
// 同步通知 UI 更新狀態列
if (touristUI != null) {
touristUI.updateProviderState(newState);
}
}
private void processLocation(Location location) {
if (location == null || !location.isValid()) return;
QualifiedCoordinates qc = location.getQualifiedCoordinates();
if (qc == null) return;
double lat = qc.getLatitude();
double lon = qc.getLongitude();
float speed = location.getSpeed(); // m/s,無效時為 Float.NaN
float course = location.getCourse(); // 度,無效時為 Float.NaN
AddressInfo addr = location.getAddressInfo(); // 可能為 null
long timestamp = location.getTimestamp(); // UTC ms
// 1. 更新主畫面
if (touristUI != null) {
touristUI.updateLocation(lat, lon, speed, course, addr, timestamp);
}
// 2. 更新指南針 (需速度/方向有效)
if (compassUI != null && !Float.isNaN(course)) {
compassUI.updateAzimuth(course);
}
// 3. 觸發首次位置更新事件 (僅第一次)
if (statusListener != null && firstUpdate) {
firstUpdate = false;
statusListener.firstLocationUpdateEvent();
}
}
private boolean firstUpdate = true;
// ==================== ProximityListener 介面 ====================
public void proximityEvent(Coordinates coordinates, Location location) {
// 進入/離開地標半徑觸發
new Thread() {
public void run() {
// 從座標反查地標名稱
String name = ControlPoints.getInstance().findLandmarkName(coordinates);
if (touristUI != null) {
touristUI.showProximityAlert(name); // 彈出警示視窗
}
}
}.start();
}
public void monitoringStateChanged(boolean isMonitoringActive) {
// 監控啟用/停用狀態 (如 GPS 關閉導致失效)
if (touristUI != null) {
touristUI.updateProximityMonitoring(isMonitoringActive);
}
}
}
locationUpdated / proximityEvent 在 **Provider 實作執行緒** 呼叫,**禁止** 直接操作 UI、存取 RMS、發送網路請求。new Thread() { run() { ... } }.start() 卸載至背景執行緒。Display.callSerially() 或確保 UI 方法內部為執行緒安全 (TouristRoute 範例直接呼叫,實務上建議用 callSerially)。package com.nokia.example.location.tourist.model;
import javax.microedition.location.*;
import java.io.IOException;
import java.util.Enumeration;
public class ControlPoints {
private static final String STORE_NAME = "TouristRoute";
private static ControlPoints instance = null;
private LandmarkStore store = null;
private ControlPoints() { initStore(); }
public static ControlPoints getInstance() {
if (instance == null) instance = new ControlPoints();
return instance;
}
private void initStore() {
try {
store = LandmarkStore.getInstance(STORE_NAME);
if (store == null) {
LandmarkStore.createLandmarkStore(STORE_NAME);
store = LandmarkStore.getInstance(STORE_NAME);
}
} catch (Exception e) {
// fallback to default store (no name)
try { store = LandmarkStore.getInstance(); }
catch (Exception ignored) {}
}
}
// ---- CRUD ----
public void addLandmark(String name, String desc, QualifiedCoordinates qc, AddressInfo addr, String category) {
try {
Landmark lm = new Landmark(name, desc, qc, addr);
store.addLandmark(lm, category);
// 新增後立即註冊 ProximityListener
if (qc != null) {
try { LocationProvider.addProximityListener(TouristData.getSharedInstance(), qc, 50); }
catch (LocationException ignored) {}
}
} catch (LandmarkException e) { /* name/desc 過長或重複 */ }
}
public void updateLandmark(Landmark lm) {
try { store.updateLandmark(lm); }
catch (LandmarkException e) { /* lm 不屬於此 store */ }
}
public void deleteLandmark(Landmark lm) {
try { store.deleteLandmark(lm); }
catch (LandmarkException e) { /* lm 不存在 */ }
}
public void removeLandmarkFromCategory(Landmark lm, String category) {
try { store.removeLandmarkFromCategory(lm, category); }
catch (LandmarkException e) { /* category 不存在 */ }
}
// ---- 查詢 ----
public Enumeration getAllLandmarks() throws IOException {
return store.getLandmarks(); // 所有地標
}
public Enumeration getLandmarks(String category, double minLat, double maxLat, double minLon, double maxLon)
throws IOException {
return store.getLandmarks(category, minLat, maxLat, minLon, maxLon);
}
public Enumeration getLandmarksByCategory(String category) throws IOException {
return store.getLandmarks(category);
}
public String findLandmarkName(Coordinates coords) {
// 簡易線性搜尋 (地標數通常 < 100)
try {
Enumeration e = store.getLandmarks();
while (e.hasMoreElements()) {
Landmark lm = (Landmark) e.nextElement();
QualifiedCoordinates qc = lm.getQualifiedCoordinates();
if (qc != null && qc.equals(coords)) return lm.getName();
}
} catch (IOException ignored) {}
return "Unknown";
}
// ---- 分類管理 ----
public Enumeration getCategories() { return store.getCategories(); }
public void addCategory(String category) { store.addCategory(category); }
public void deleteCategory(String category) { store.deleteCategory(category); }
}
public class TouristUI extends Form implements CommandListener {
private TouristData data;
private StringItem coordItem, speedItem, courseItem, addressItem;
private Command cmdEdit, cmdCompass, cmdLandmarks, cmdExit;
public TouristUI(TouristData data) {
super("Route View");
this.data = data;
data.setTouristUI(this);
coordItem = new StringItem("Coordinates", "Waiting...");
speedItem = new StringItem("Speed", "0.0 km/h");
courseItem = new StringItem("Course", "N/A");
addressItem = new StringItem("Address", "Unknown");
append(coordItem); append(speedItem); append(courseItem); append(addressItem);
cmdEdit = new Command("Edit", Command.SCREEN, 1);
cmdCompass = new Command("Compass", Command.SCREEN, 2);
cmdLandmarks = new Command("Landmarks", Command.SCREEN, 3);
cmdExit = new Command("Exit", Command.EXIT, 1);
addCommand(cmdEdit); addCommand(cmdCompass); addCommand(cmdLandmarks); addCommand(cmdExit);
setCommandListener(this);
}
public void updateLocation(double lat, double lon, float speed, float course, AddressInfo addr, long ts) {
coordItem.setText(Utils.formatDouble(lat,5) + ", " + Utils.formatDouble(lon,5));
speedItem.setText(Float.isNaN(speed) ? "N/A" : (speed*3.6) + " km/h"); // m/s → km/h
courseItem.setText(Float.isNaN(course) ? "N/A" : Utils.formatDouble(course,1) + "°");
if (addr != null) {
StringBuilder sb = new StringBuilder();
for (int i=0; i0) sb.append(v).append(", ");
}
addressItem.setText(sb.length()>0 ? sb.substring(0,sb.length()-2) : "Unknown");
}
}
public void commandAction(Command c, Displayable d) {
if (c == cmdEdit) {
data.setLandmarkEditorUI(new LandmarkEditorUI(data));
} else if (c == cmdCompass) {
if (ConfigurationProvider.getInstance().isOrientationSupported()) {
data.setCompassUI(new CompassUI(data));
} else {
new Alert("Not Supported", "Device has no compass", null, AlertType.WARNING).show();
}
} else if (c == cmdLandmarks) {
data.setLandmarkEditorUI(new LandmarkEditorUI(data)); // 同編輯器,但模式不同
} else if (c == cmdExit) {
data.getMIDlet().notifyDestroyed();
}
}
}
public class CompassUI extends Canvas {
private TouristData data;
private float azimuth = 0; // 0-360 度
public CompassUI(TouristData data) {
this.data = data;
data.setCompassUI(this);
addCommand(new Command("Back", Command.BACK, 1));
setCommandListener(this);
setFullScreenMode(true);
}
public void updateAzimuth(float az) {
this.azimuth = az;
repaint(); // 觸發 paint()
}
protected void paint(Graphics g) {
int w = getWidth(), h = getHeight();
int cx = w/2, cy = h/2, r = Math.min(w,h)/2 - 10;
// 背景
g.setColor(0xFFFFFF); g.fillRect(0,0,w,h);
// 圓形刻度
g.setColor(0x000000); g.drawArc(cx-r, cy-r, 2*r, 2*r, 0, 360);
// 方位標示 N/E/S/W
g.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_SMALL));
drawTextAtAngle(g, "N", cx, cy, -90, r+15);
drawTextAtAngle(g, "E", cx, cy, 0, r+15);
drawTextAtAngle(g, "S", cx, cy, 90, r+15);
drawTextAtAngle(g, "W", cx, cy, 180, r+15);
// 指針 (紅色三角形)
g.setColor(0xFF0000);
int[] x = {cx, cx + (int)(r*0.8*Math.cos(Math.toRadians(azimuth-90))),
cx + (int)(r*0.3*Math.cos(Math.toRadians(azimuth-90+150)))};
int[] y = {cy, cy + (int)(r*0.8*Math.sin(Math.toRadians(azimuth-90))),
cy + (int)(r*0.3*Math.sin(Math.toRadians(azimuth-90+150)))};
g.fillTriangle(x[0],y[0], x[1],y[1], x[2],y[2]);
// 數值顯示
g.setColor(0x000000);
g.drawString("Azimuth: " + Utils.formatDouble(azimuth,1) + "°", 5, 5, Graphics.TOP|Graphics.LEFT);
}
private void drawTextAtAngle(Graphics g, String s, int cx, int cy, int angleDeg, int radius) {
double rad = Math.toRadians(angleDeg);
int x = cx + (int)(radius * Math.cos(rad)) - g.getFont().stringWidth(s)/2;
int y = cy + (int)(radius * Math.sin(rad)) - g.getFont().getHeight()/2;
g.drawString(s, x, y, Graphics.TOP|Graphics.LEFT);
}
public void commandAction(Command c, Displayable d) {
if (c.getCommandType() == Command.BACK) {
data.setCompassUI(null);
Display.getDisplay(data.getMIDlet()).setCurrent(data.getTouristUI());
}
}
}
public class PitchRollUI extends Canvas {
private TouristData data;
private float pitch = 0, roll = 0; // -90~90, -180~180
public PitchRollUI(TouristData data) {
this.data = data;
data.setPitchRollUI(this);
addCommand(new Command("Back", Command.BACK, 1));
setCommandListener(this);
setFullScreenMode(true);
}
public void updatePitchRoll(float p, float r) {
this.pitch = p; this.roll = r;
repaint();
}
protected void paint(Graphics g) {
int w = getWidth(), h = getHeight();
int cx = w/2, cy = h/2;
int size = Math.min(w,h)/2 - 10;
g.setColor(0xFFFFFF); g.fillRect(0,0,w,h);
// 參考水平線 (綠)
g.setColor(0x00FF00);
int yLine = cy + (int)(pitch * size / 90.0);
g.drawLine(0, yLine, w, yLine);
// 翻滾指示器 (紅色線段,隨 roll 旋轉)
g.setColor(0xFF0000);
double rad = Math.toRadians(roll);
int x1 = cx - (int)(size * Math.cos(rad));
int y1 = cy - (int)(size * Math.sin(rad));
int x2 = cx + (int)(size * Math.cos(rad));
int y2 = cy + (int)(size * Math.sin(rad));
g.drawLine(x1, y1, x2, y2);
// 文字
g.setColor(0x000000);
g.drawString("Pitch: " + Utils.formatDouble(pitch,1) + "°", 5, 5, Graphics.TOP|Graphics.LEFT);
g.drawString("Roll: " + Utils.formatDouble(roll,1) + "°", 5, 20, Graphics.TOP|Graphics.LEFT);
}
public void commandAction(Command c, Displayable d) {
if (c.getCommandType() == Command.BACK) {
data.setPitchRollUI(null);
Display.getDisplay(data.getMIDlet()).setCurrent(data.getCompassUI());
}
}
}
ControlPoints.addLandmark)、Delete (刪除)、Back。Landmark 資料;新增模式自動帶入當前 GPS 座標與地址。
使用者啟動 App
│
▼
TouristMIDlet.startApp()
│
▼
ConfigurationProvider.autoSearch() ──找到 Provider──▶ providerSelectedEvent()
│ │
│ ▼
│ TouristMIDlet.providerSelectedEvent()
│ │
│ ▼
│ new TouristData(this)
│ │
│ ▼
│ TouristData.registerListeners()
│ │
│ ▼
│ LocationProvider.setLocationListener()
│ │
│ (背景) GPS 定位成功
│ │
│ ▼
│ TouristData.locationUpdated()
│ │
│ ▼
│ new Thread → processLocation()
│ │
│ ▼
│ touristUI.updateLocation()
│ │
▼ ▼
firstLocationUpdateEvent() ◀─────────────────────────┘
│
▼
new TouristUI(data) → display.setCurrent() → 進入主畫面
│
▼
使用者按「Compass」 → new CompassUI(data) → 顯示指南針
│
▼
使用者按「Pitch/Roll」 → new PitchRollUI(data) → 顯示姿態
│
▼
使用者按「Landmarks」 → new LandmarkEditorUI(data) → 編輯/新增地標
│
▼
Save → ControlPoints.addLandmark() → LandmarkStore + 註冊 ProximityListener
jsr179.jar 編譯、預驗證通過。MIDlet-Permissions: javax.microedition.location.Location、javax.microedition.location.Landmark。三篇文章完整涵蓋:
此專案展示了 **JSR-179 所有核心功能** 的最佳實踐,可直接作為商業 LBS 應用的骨架程式碼基礎。