← 都来码 / Dolaima← 例程索引c/app/locate.c原文

c/app/locate.c · 22771 字节 · 下载原文

/* locate.c —— 常用小程序⑩:**定位 / GPS**(第 4 层:`ui_*` 图形界面)
 * locate.c -- Everyday utility #10: **location / GPS** (layer 4: the `ui_*` graphical interface)
 *
 * 干什么:`ui_gps_fetch` 取一次定位,把**纬度 / 经度**用大字打出来,下面是海拔与精度;
 * What it does: `ui_gps_fetch` takes one fix, the **latitude and longitude** are printed large, and
 * 底下画一块**示意**的"地图角"(十字 + 一个小圆点)。
 * the altitude and accuracy follow underneath, over a **schematic** "map corner" (a crosshair plus a
 * dot).
 *
 * ## ⚠ 坐标是**定点整数**,全程不碰浮点
 * ## ⚠ The coordinates are **fixed-point integers**; no floating point anywhere
 *
 * `ui_gps_get_lat_e6()` 是**纬度 × 1e6**(`31523000` = `31.523000` 度),经度同理;
 * `ui_gps_get_lat_e6()` is **latitude x 1e6** (`31523000` = `31.523000` degrees) and longitude is the
 * 海拔与精度是**米的整数**。所以格式化是纯整数活:整数部分 `v / 1000000`、
 * same; altitude and accuracy are **integer metres**. Formatting is therefore pure integer work: the
 * 小数部分 `v % 1000000` 取六位(**补零**,见 `deg6`)—— 第 4 层里根本没有 `%f`,
 * integer part is `v / 1000000` and the fraction `v % 1000000` is printed as six zero-padded digits
 * 而且 22 门语言共用一套 ABI,浮点那条路上还埋着"浮点当实参传位模式"的坑(见 `level.c` 坑 ③)。
 * (see `deg6`). Layer 4 has no `%f` at all, the 22 languages share one ABI, and the floating-point
 * path carries this repo's "float argument passed as a bit pattern" trap (pitfall 3 in `level.c`).
 *
 * ## ⚠ 手机上才有 —— 桌面(vmlcli)上「本机没有 GPS」才是**正确输出**
 * ## ⚠ Phone-only -- on the desktop (vmlcli) "no GPS on this device" is the **correct output**
 *
 * 真机上几乎每台手机都能定位(没给权限时 `ui_gps_is_available()` 报 0);而桌面脚手架**没有
 * Almost every real phone can locate (without the permission `ui_gps_is_available()` reports 0), while
 * GPS 硬件**,所以本条也是 0。于是本文件的**第一条分支**不是"读数对不对",而是
 * the desktop scaffold has **no GPS hardware**, so it reports 0 as well. The first branch here is
 * "**这台机器根本没有 GPS**",界面上必须**明说**,而不是画一个 `0.000000 / 0.000000` 的假坐标
 * therefore not "is the reading right" but "**this machine has no GPS at all**", and the screen must
 * —— 那会让人以为"你真在几内亚湾"(0,0 就在那里)。
 * **say so** instead of drawing a fake `0.000000 / 0.000000`, which would place you in the Gulf of
 * Guinea. `0` is not an error: a timeout, a missing permission and a missing chip all look the same.
 *
 * ## 桌面怎么看到"有定位"那一半:宿主自带的输入脚本注入,**不用改一行源码**
 * ## Seeing the located branch on the desktop: the host's input-script injection, **no source change**
 *
 *     printf '0 gps 31523000 121470000 12 8\n' > in.txt
 *     vmlcli locate.c --vml-home third_party/vml --input in.txt --timeout 40 --frame locate.png
 *
 * 这四个数就是 `lat_e6 lon_e6 alt_m acc_m`,喂给宿主(`CliVmlHost.InjectLocation`)之后
 * Those four numbers are `lat_e6 lon_e6 alt_m acc_m`; once fed to the host (`CliVmlHost.InjectLocation`)
 * `ui_gps_is_available()` 报 1、`ui_gps_fetch` 成功,四个 get 就交回这四个数 —— 换一组数就是另一张图。
 * `ui_gps_is_available()` reports 1, `ui_gps_fetch` succeeds and the four getters return them. Feeding
 * ⚠ 注入只影响**脚手架**:手机端那条真实现(Geolocation + 权限)一个字节都不动。
 * another set of numbers gives another picture. The injection only affects the **scaffold**.
 *
 * ## 踩到的坑(都是这一轮真撞上的,写在这里省得下一个人再撞)
 * ## Pitfalls (all hit for real while writing this file; recorded so the next person does not)
 *
 * ① ⚠ **取数顺序不能乱**:`ui_gps_get_*` 读的是**上一次 `ui_gps_fetch` 的结果**(没取过就是 0),
 * ① ⚠ **The order of calls is not free**: `ui_gps_get_*` return the result of the **last
 *   `ui_gps_fetch`** (0 if it never succeeded). So this file asks `ui_gps_is_available()` first, then
 *   —— 本文件先问"有没有",`fetch` 成功之后**紧接着**读那四个数,中间不加别的调用
 *   calls `ui_gps_fetch`, and reads those four numbers **immediately after** it without anything in
 *   (与 `level.c` 坑 ① 的传感器缓存同一条规矩)。
 *   between (the same rule as the sensor cache in pitfall 1 of `level.c`).
 * ② ⚠ **别把 `printf` 一整行里的几个调用当"从左到右"**(实测踩到):`printf("%d %d", fetch(), get())`
 * ② ⚠ **Never assume the calls inside one `printf` run left to right** (hit for real): with
 *   的实参求值顺序由编译器定 —— 实测 `get` 先跑,于是读到的是**取之前**的 0,
 *   `printf("%d %d", fetch(), get())` the argument order is the compiler's to choose; measured, `get`
 *   看起来像"fetch 成功了但坐标全是 0"(接口一点毛病没有)。所以取数与使用**拆成两条语句**。
 *   ran first and read the zeros from **before** the fetch, which looks exactly like "the fetch worked
 *   but the coordinates are all 0". Split the call and the use into two statements.
 * ③ ⚠ **示意地图只是示意**:那个点按**小数部分**摆,与真实地理位置**没有任何关系**
 * ③ ⚠ **The schematic map is only a sketch**: the dot is placed by the **fractional part** and has
 *   (真地图要投影、要底图、要比例尺,那是另一个程序的事)。所以旁边写着「示意」两个字,
 *   **no relation** to the real position (a real map needs a projection, a base map and a scale bar,
 *   and that is a different program). The box is therefore labelled "sketch" in the UI, and the label
 *   and the dot is positioned the same way in every run.
 * ④ ⚠ **"有没有 GPS"每拍都要重问一次**(与 `level.c` 坑 ② 同源):`--input` 的事件由另一个线程按
 * ④ ⚠ **"Is GPS there" must be re-asked every tick** (same as pitfall 2 in `level.c`): the `--input`
 *   时刻投递,程序可能在注入落地之前就问过了。所以"没有"每拍重问,一旦有就不再问。
 *   events arrive from another thread on a timer, and the program may ask before the injection lands.
 *   Absent is re-asked every tick; once present it is not asked again.
 * ⑤ ⚠ **文件级标量别用负值初值**(本仓刚归档的 E187):`static int x = -1;` 读到的是 **0**。
 * ⑤ ⚠ **Never give a file-scope scalar a negative initializer** (this repo's E187): `static int x = -1;`
 *   所以下面所有哨兵量一律用**正数**(0 = 没有/未知,1 = 有),别拿 -1 当"还没有"。
 *   reads back as **0**. Every sentinel below is a **positive** number (0 = absent/unknown, 1 = present).
 *
 * 版面:尺寸一律从宿主问(`ui_scr_get_w()/_h()`),颜色是真彩 `0xAARRGGBB`,画完必须 `ui_2d_present()`。
 * Layout: every size is asked of the host (`ui_scr_get_w()/_h()`), colours are true colour
 * 循环**必须有界**(例程要能被自动验收):空闲 10 秒自己退出,`Esc`/`q` 也退出。
 * `0xAARRGGBB`, and the frame must end with `ui_2d_present()`. The loop **must be bounded** (samples
 * 写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
 * have to be automatically verifiable): it exits after 10 s of idling, and `Esc`/`q` quits too.
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */

#define IDLE_EXIT_MS 10000        /*  空闲 10 秒自己退出(验收用) */
                                  /* idle exit after 10 s (for automated checks) */

#define FETCH_MS 2000             /*  取一次定位的超时(真机上是**头一次定位**的等待)*/
                                  /* timeout for one fix (on a phone this is the first-fix wait) */

/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG      0xFF0E1218
#define C_BOX     0xFF141A22
#define C_RING    0xFF3C4658
#define C_GRID    0xFF2A3346
#define C_TITLE   0xFFE8E8F0
#define C_DIM     0xFF8A94A8
#define C_ERR     0xFFFF8A80     /*  没有这项能力 */
                                 /* capability absent */
#define C_LAT     0xFF6FC3FF     /*  纬度 */
                                 /* latitude */
#define C_LON     0xFF4ADE80     /*  经度 */
                                 /* longitude */
#define C_DOT     0xFFFFD166     /*  示意地图上的那个点 */
                                 /* the dot on the schematic map */

/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;
static int SH;
static int g_pad;
static int lang;

static int g_avail;               /*  1 = 这台机器有定位(每拍重问一次,见坑 ④)*/
                                 /* 1 = this machine can locate (re-asked every tick, pitfall 4) */
static int g_ok;                  /*  1 = 取到过一次定位(四个读数有效)*/
                                 /* 1 = a fix was taken once (the four readings are valid) */
static int g_lat;                 /*  纬度 × 1e6 */
                                 /* latitude x 1e6 */
static int g_lon;                 /*  经度 × 1e6 */
                                 /* longitude x 1e6 */
static int g_alt;                 /*  海拔(米)*/
                                 /* altitude in metres */
static int g_acc;                 /*  水平精度(米)*/
                                 /* horizontal accuracy in metres */

/* 版面(画与命中判定**必须用同一份**)*/
/* Layout (drawing and hit testing **must share this one copy**) */
static int mX;
static int mY;
static int mW;
static int mH;                    /*  示意地图那块方框 */
                                 /* the schematic map box */
static int readY;                 /*  大字号那两行(纬度 / 经度)的起始 y */
                                 /* the y where the two large lines (latitude / longitude) start */

/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */

static char g_i10[16];
static char *numstr(int v) {
    itoa(v, g_i10);
    return g_i10;
}

/* ×1e6 的定点数 → "31.523000"(符号 + 整数部分 + **六位**小数,缺位补零)。
 * A x1e6 fixed-point value to "31.523000" (sign, integer part, then **six** zero-padded decimals).
 * ⚠ 返回的是**同一个静态缓冲** —— 别在一个表达式里连用两次(`strcat` 两次会互相覆盖)。
 * ⚠ It returns **one static buffer** -- never use it twice in one expression (the second call
 * overwrites the first before `strcat` copies it).
 * ⚠ 负零:`-0.000400` 要写成 `-0.000400`(实测 `-400 / 1000000` 在 C 里是 0、靠 `neg` 那一位才不丢符号)。
 * ⚠ Negative zero: `-0.000400` must keep its sign, which C's integer division would drop (`-400/1000000`
 * is 0), so the sign is carried separately.
 */
static char g_deg[24];
static char *deg6(int e6) {
    int i;
    int n;
    int neg;
    int ip;
    int fp;
    char tmp[16];
    neg = 0;
    if (e6 < 0) { neg = 1; e6 = 0 - e6; }
    ip = e6 / 1000000;
    fp = e6 - ip * 1000000;
    i = 0;
    if (neg != 0) { g_deg[i] = '-'; i = i + 1; }
    n = 0;
    if (ip == 0) { tmp[n] = '0'; n = n + 1; }
    while (ip > 0) { tmp[n] = (char)('0' + ip % 10); ip = ip / 10; n = n + 1; }
    while (n > 0) { n = n - 1; g_deg[i] = tmp[n]; i = i + 1; }
    g_deg[i] = '.'; i = i + 1;
    g_deg[i] = (char)('0' + (fp / 100000) % 10); i = i + 1;
    g_deg[i] = (char)('0' + (fp / 10000) % 10); i = i + 1;
    g_deg[i] = (char)('0' + (fp / 1000) % 10); i = i + 1;
    g_deg[i] = (char)('0' + (fp / 100) % 10); i = i + 1;
    g_deg[i] = (char)('0' + (fp / 10) % 10); i = i + 1;
    g_deg[i] = (char)('0' + fp % 10); i = i + 1;
    g_deg[i] = 0;
    return g_deg;
}

/* ── 取一次定位 ─────────────────────────────────────────────────────── */
/* -- Taking one fix -- */

/* 返回 1 = 画面要重画。⚠ 顺序:先问"有没有",再 `fetch`,**紧接着**读那四个数(见坑 ①)。
 * Returns 1 when the screen needs a redraw. ⚠ Order: ask "is it there", then `fetch`, then read the
 * "没有"每拍重问(注入与本程序启动之间有赛跑,见坑 ④);取到过就不再重复取。
 * four numbers **immediately** (pitfall 1). Absent is re-asked every tick (the injection race,
 * pitfall 4); once a fix has been taken it is not taken again.
 */
static int poll(void) {
    int changed;
    changed = 0;
    if (g_avail == 0) {
        g_avail = ui_gps_is_available();
        if (g_avail != 0) changed = 1;
    }
    if (g_avail != 0 && g_ok == 0) {
        if (ui_gps_fetch(FETCH_MS) != 0) {
            /* ⚠ 取数与使用**拆成两条语句**(见坑 ②)*/
            /* ⚠ The call and the use stay in separate statements (pitfall 2) */
            g_lat = ui_gps_get_lat_e6();
            g_lon = ui_gps_get_lon_e6();
            g_alt = ui_gps_get_alt_m();
            g_acc = ui_gps_get_accuracy_m();
            g_ok = 1;
            changed = 1;
        }
    }
    return changed;
}

/* ── 版面 ───────────────────────────────────────────────────────────── */
/* -- Layout -- */

static void layout(void) {
    int below;
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;
    g_pad = SW / 18;
    if (g_pad < 10) g_pad = 10;

    readY = g_pad + 56;                   /*  标题两行之下就是那两行大字 */
                                          /* the two large lines start right below the two heading lines */

    /* 示意地图:**正方形**(十字要对称),边长同时受宽与高约束 ——
     * The schematic map is a **square** (a crosshair must be symmetric), bounded by both width and
     * 只看宽度的话矮屏(横屏)上它会压住上面两行读数。
     * height; width alone would push it over the readouts on a short (landscape) screen.
     */
    below = readY + 116;                  /*  大字两行 + 海拔/精度一行 + 一点间距 */
                                          /* two large lines plus the altitude/accuracy line and a gap */
    mW = SW - g_pad * 2;
    if (mW > 260) mW = 260;
    mH = mW;
    if (mH > SH - g_pad - 34 - below) mH = SH - g_pad - 34 - below;
    if (mH < 70) mH = 70;
    mW = mH;
    mX = (SW - mW) / 2;
    mY = below;
}

/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* -- Drawing -- */

/* 大字那两行 + 海拔/精度一行 */
/* The two large lines plus the altitude/accuracy line */
static void drawReadout(void) {
    char buf[64];
    buf[0] = 0;
    strcat(buf, lang == 0 ? "纬度  " : "lat   ");
    strcat(buf, deg6(g_lat));
    ui_2d_text(SW / 2, readY, buf, C_LAT, 26, VML_ANCHOR_CENTER);

    buf[0] = 0;
    strcat(buf, lang == 0 ? "经度  " : "lon   ");
    strcat(buf, deg6(g_lon));
    ui_2d_text(SW / 2, readY + 34, buf, C_LON, 26, VML_ANCHOR_CENTER);

    buf[0] = 0;
    strcat(buf, lang == 0 ? "海拔 " : "alt ");
    strcat(buf, numstr(g_alt));
    strcat(buf, lang == 0 ? " 米     精度 ±" : " m     accuracy +-");
    strcat(buf, numstr(g_acc));
    strcat(buf, lang == 0 ? " 米" : " m");
    ui_2d_text(SW / 2, readY + 74, buf, C_DIM, 14, VML_ANCHOR_CENTER);
}

/* 示意地图:方框 + 十字 + 一个点。
 * The schematic map: a box, a crosshair and one dot.
 * ⚠ **它只是示意**(见文件头坑 ③):点按**小数部分**摆,与真实地理位置无关 ——
 * ⚠ **It is only a sketch** (pitfall 3): the dot is placed by the **fractional part** and has nothing
 * 真地图要投影 + 底图 + 比例尺,那是另一个程序的事。方框上写着「示意」就是为了不骗人。
 * to do with the real position (a real map needs a projection, a base map and a scale bar). The box
 * says "sketch" so that it cannot be mistaken for one.
 */
static void drawMap(void) {
    int cx;
    int cy;
    int r;
    int fl;
    int fo;
    int dx;
    int dy;
    int dotR;
    char buf[32];

    cx = mX + mW / 2;
    cy = mY + mH / 2;
    r = mW / 2 - 4;

    ui_2d_rect(mX, mY, mW, mH, C_BOX, 1, 0, 8);
    ui_2d_rect(mX, mY, mW, mH, C_RING, 0, 1, 8);
    /* 十字与四分格:给"点在哪儿"一个参照 */
    /* A crosshair and quarters: a frame of reference for "where the dot is" */
    ui_2d_line(mX + 6, cy, mX + mW - 6, cy, C_GRID, 1);
    ui_2d_line(cx, mY + 6, cx, mY + mH - 6, C_GRID, 1);
    ui_2d_circle(cx, cy, r / 2, C_GRID, 0, 1);

    if (g_ok == 0) {
        ui_2d_text(cx, cy - 8, lang == 0 ? "没有读数" : "no fix", C_ERR, 14, VML_ANCHOR_CENTER);
    } else {
        fl = g_lat % 1000000;
        fo = g_lon % 1000000;
        if (fl < 0) fl = 0 - fl;
        if (fo < 0) fo = 0 - fo;
        dx = (fo - 500000) * r / 500000;
        dy = (fl - 500000) * r / 500000;
        dotR = mW / 28;
        if (dotR < 4) dotR = 4;
        ui_2d_circle(cx + dx, cy - dy, dotR + 3, 0x60FFD166, 1, 0);
        ui_2d_circle(cx + dx, cy - dy, dotR, C_DOT, 1, 0);
    }

    buf[0] = 0;
    strcat(buf, lang == 0 ? "示意(按小数部分摆的点)" : "sketch (dot placed by the fraction)");
    ui_2d_text(cx, mY + mH - 20, buf, C_DIM, 11, VML_ANCHOR_CENTER);
}

/* 没有 GPS 时整屏就一句话 —— 别画一个 (0,0) 的假坐标(见文件头那段)*/
/* With no GPS the whole screen is one sentence -- never a fake (0,0) coordinate (see the header) */
static void drawNoGps(void) {
    ui_2d_text(SW / 2, SH / 2 - 40, lang == 0 ? "本机没有 GPS" : "no GPS on this device",
               C_ERR, 24, VML_ANCHOR_CENTER);
    ui_2d_text(SW / 2, SH / 2 + 4, lang == 0 ? "(手机上才有)" : "(phones only)",
               C_ERR, 15, VML_ANCHOR_CENTER);
    ui_2d_text(SW / 2, SH / 2 + 40, lang == 0 ? "读数是 0 不是错误:这一端没有这个能力"
                                              : "a reading of 0 is not an error: this end lacks it",
               C_DIM, 12, VML_ANCHOR_CENTER);
}

static void draw(void) {
    ui_2d_clear(C_BG);

    ui_2d_text_styled(SW / 2, g_pad, lang == 0 ? "定位 / GPS" : "Location / GPS",
                      C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);

    if (g_avail == 0) {
        ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "ui_gps_is_available() = 0" : "ui_gps_is_available() = 0",
                   C_ERR, 12, VML_ANCHOR_CENTER);
        drawNoGps();
    } else {
        if (g_ok != 0) {
            ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "ui_gps_fetch 取到的定点整数(×1e6)"
                                                     : "fixed-point (x1e6) values from ui_gps_fetch",
                       C_DIM, 11, VML_ANCHOR_CENTER);
            drawReadout();
        } else {
            ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "有 GPS,但这一次没取到(超时 / 没权限)"
                                                     : "GPS present, but this fix failed (timeout / permission)",
                       C_ERR, 12, VML_ANCHOR_CENTER);
        }
        drawMap();
    }

    ui_2d_text(SW / 2, SH - 14, lang == 0 ? "手机上才有真读数   Esc / q 退出"
                                          : "real readings are phone-only   Esc / q quits",
               C_DIM, 11, VML_ANCHOR_CENTER);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏 */
                          /* without this call you get a black screen */
}

/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;

    lang = ui_scr_get_language();

    /* "有没有"**开窗之前就能问**,先问再决定画什么 */
    /* Availability is **askable before opening a window**; ask first, then decide what to draw */
    g_avail = ui_gps_is_available();
    poll();

    layout();
    ui_win_open_ex(lang == 0 ? "定位 / GPS" : "Location / GPS",
                   0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();                              /*  开窗之后画布尺寸才是准的,再算一次 */
                                          /* the canvas size is only final after the window opens */
    draw();

    t0 = ui_time_get_ms();
    done = 0;
    while (!done && ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 200);
        if (t == VML_MSG_NONE) {
            /* 没有消息也不代表没有变化:定位是**轮询**的(与 `level.c` 的传感器同一条)*/
            /* No message does not mean nothing changed: location is **polled** (same as the sensors in `level.c`) */
            if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1;
            else if (poll() != 0) draw();
            continue;
        }
        t0 = ui_time_get_ms();
        if (t == VML_MSG_KEYDOWN) {
            int k = msg[1];
            if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
            layout();
            draw();
            continue;
        }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }
    return 0;
}