首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2ME

J2ME 定位 API 範例實作 (三):TouristData 事件處理、地標管理與指南針/姿態視覺化完整原始碼

     編輯:J2ME

TouristData — LocationListener 與 ProximityListener 雙重實作

核心職責:接收非同步位置更新與接近事件,轉發給對應 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() 卸載至背景執行緒。
  • UI 更新需呼叫 Display.callSerially() 或確保 UI 方法內部為執行緒安全 (TouristRoute 範例直接呼叫,實務上建議用 callSerially)。

ControlPoints — LandmarkStore 完整封裝

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); }
}

TouristUI — 主畫面繪製與指令處理 (繼承 Form)

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();
        }
    }
}

CompassUI — 指南針視覺化 (繼承 Canvas)

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());
        }
    }
}

PitchRollUI — 俯仰/翻滾 3D 姿態視覺化

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());
        }
    }
}

LandmarkEditorUI — 地標編輯器 (繼承 Form)

  • 欄位:名稱、描述、類別 (ChoiceGroup)、地址欄位 (Country/State/City/Street/Building)。
  • 指令:Save (存入 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 編譯、預驗證通過。
  • [ ] JAD 宣告 MIDlet-Permissions: javax.microedition.location.Location、javax.microedition.location.Landmark。
  • [ ] 真機測試:GPS 冷啟動、室內 Cell-ID、Proximity 警示、指南針校準、地標跨 MIDlet 共享。
  • [ ] Jar 大小 < 目標裝置上限 (S60 通常 1-2 MB)。

系列總結

三篇文章完整涵蓋:

  1. 架構與啟動序列:MIDlet/UI/Model 分層、同步機制、套件結構。
  2. 核心 Model 實作:Utils、ConfigurationProvider (Criteria/Provider 搜尋)、Orientation、ProviderQueryUI。
  3. 事件處理與視覺化:TouristData (雙 Listener 背景執行緒)、ControlPoints (LandmarkStore CRUD)、TouristUI/CompassUI/PitchRollUI/LandmarkEditorUI 完整原始碼。

此專案展示了 **JSR-179 所有核心功能** 的最佳實踐,可直接作為商業 LBS 應用的骨架程式碼基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved