Skip to content

Prajuab Riabroy's Blog

Survey Application – GNSS – UAV – Programing – Python – Dart&Flutter – Rust

Menu
  • Home
  • About Us
  • Survey Application
    • Surveyor Pocket Tools
    • Traverse Pro
    • XSection Plot
    • Geoid Height Pro
    • Spot Fire
    • โปรแกรมงานสำรวจสำหรับเครื่องคิดเลข Casio fx-5800P
    • โปรแกรมงานสำรวจสำหรับเครื่องคิดเลข Casio fx-9860G II SD
  • Download
  • Privacy Policy
Menu

ปฐมฤกษ์โปรแกรมเดสก์ท็อปภาษา Rust ด้วย Slint

Posted on 28/11/2024

หลังจากที่ได้ศึกษาภาษา Rust ในสามเดือน สี่เดือนที่ผ่านมาอย่างเอาจริงเอาจัง ถึงแม้จะรู้สึกว่ายากแต่ก็ฟันฝ่าจนจบหลักสูตรทั้งอ่านหนังสือ Rust Language Book และดูคลิปยูทูปเพื่อศึกษาและทบทวนจำนวน 14 ชั่วโมง แต่ดูจริงๆวันละไม่เกิน 30 นาทีหรือน้อยกว่านั้นเพราะต้องทดลองทำโจทย์ตามผู้สอนด้วย จบหลักสูตรยูทูปประมาณเดือนหนึ่งเต็มๆ โดยเฉพาะการดูคลิปนี้สอนได้ดีมาก ค่อยๆสอนไปอย่างไม่รีบร้อน อธิบายได้ละเอียดจนอะไรที่สงสัยตอนอ่านหนังสือก็มากระจ่างจากการดูคลิปนี้

การอ่านหนังสือเหมือนได้ทฤษฎีมาเปล่าๆ การลงมือปฏิบัติคือเป็นการบูรณาการว่าความรู้ที่ได้มานั้นเพียงพอจะเขียนโปรแกรมหนึ่งหรือไม่อย่างไร ความรู้นั้นสามารถต่อจิ๊กซอว์ได้เต็มกระดานหรือไม่

ผมค้นหาคลิปที่คนศึกษาภาษา Rust ได้ใหม่ๆแล้วลงมือเขียนโปรแกรมบน Desktop และมาเจอกับสองคลิปที่คล้ายๆกันคือศึกษาภาษา Rust แล้วลงมือเขียนโปรแกรมด้วย Rust สิ่งที่ต้องคำนึงถึงอีกประการคือจะใช้อะไรที่เป็น GUI ทั้งสองคนในคลิปใจตรงกันคือใช้ Slint ด้วยเหตุนี้ ผมก็ตาม Slint ไปติดๆ

ส่วนติดต่อผู้ใช้งานแบบกราฟิกด้วย Slint

ตัว Slint เขียนขึ้นด้วยภาษา Rust หลักการทำงานคือแยกไฟล์ UI ออกมาต่างหาก คล้ายๆกับ QML ของ Qt โครงการ Slint ก็ก่อกำเนิดขึ้นไม่นานนัก สามารถเขียนโปรแกรม desktop แบบ cross-platform ลงวินโดส์ ลีนุกซ์และแมคโอเอสได้ ไทม์ไลน์บอกว่าจะสามารถเขียนโปรแกรมลงโทรศัพท์มือถือเช่นแอนดรอยด์และไอโอเอส และในตอนนี้สามารถเขียน Rust ลง embedded หรือไมโครคอนโทรลเลอร์ ส่วนค่าไลเซนต์มีหลายระดับแยกย่อยไปหลายระดับตั้งแต่ฟรีจนถึงพรีเมียม

เท่าที่สัมผัส Slint ในเบื้องต้นคือยังห่างไกลกับ Qt มาก แต่เมื่อจะลองใช้งานดูแล้ว ศึกษาภาษา Slint เพื่อขึ้นรูป UI ก็ดูไม่ใช่เรื่องหนักหนาอะไร พอศึกษาไปสักพักก็เข้าใจคอนเซ็ปต์

ก่อนหน้านี้ผมใช้ไลบรารี Rust Geodesy (RG) เพื่อทดสอบการแปลงพิกัดระหว่างเส้นโครงความคลาดเคลื่อนต่ำ TM ไปยังพื้นหลักฐาน WGS84 และ Indian 1975 ในช่วงดีบั๊กนั้นมีการแปลงพิกัดภูมิศาสตร์ () ไปยังค่าพิกัดคาร์ทีเซียน (X, Y, Z) ที่เราเรียกว่า Geocentric ต้องไปหาทูลส์มาแปลงพิกัดและไปจบที่ทูลส์ออนไลน์ หมายเหตุการแปลงพิกัดนี้ผมไม่ได้ลงไว้ใน Surveyor Pocket Tools

สูตรการแปลงพิกัด

ก็เลยมีแรงบันดาลใจที่จะเขียนโปรแกรมนี้เพื่อทดสอบความรู้ภาษา Rust สำหรับสูตรในการแปลงพิกัดภูมิศาสตร์ไปยัง Geocentric ไม่ยาก ตรงไปตรงมา

สูตรการแปลงพิกัดจาก Geographic เป็น Geocentric
 x = (N + h) \cdot \cos(\phi) \cdot \cos(\lambda)

 y = (N + h) \cdot \cos(\phi) \cdot \sin(\lambda)

 z = \left(N \cdot (1 - E^2) + h\right) \cdot \sin(\phi)

โดยที่

 N = \frac{A}{\sqrt{1 - E^2 \cdot \sin^2(\phi)}}

E เป็นค่า eccentricity ของทรงรี (WGS84: 0.081819190842622)

A เป็นระยะของกึ่งแกนเอก (WGS84: 6378137 เมตร),

สูตรการแปลงพิกัดจาก Geocentric เป็น Geographic

สูตรการแปลงจาก Geocentric ไป Geographic จะไม่เรียบง่ายเพราะต้องมีการวนลูป เพื่อหาค่าละติจูด

\lambda = \text{atan2}(y, x)

p = \sqrt{x^2 + y^2}

วนลูปเพื่อหาค่าละติจูด

\phi_{\text{new}} = \text{atan}\left(\frac{z + E^2 \cdot N \cdot \sin(\phi)}{p}\right)

จนกระทั่ง

\left|\phi_{\text{new}} - \phi\right|\le \epsilon

หาค่า h

h = \frac{p}{\cos(\phi)} - N

โค้ดแปลงพิกัด

สำหรับฟังก์ชันหาค่าการแปลงพิกัดดังกล่าว มีกระจายทั่วในอินเทอร์เน็ต ถ้าอ่านสูตรแล้วพยายามเขียน Rust เองคงใช้เวลาพักใหญ่ๆ ผมใช้ตัวช่วยคือขอให้ ChatGPT เขียนโค้ดมาให้ หน้าที่เราคือเอาโค้ดไปแปะใน Rust Playground แล้วทดสอบหาผลลัพธ์ เมื่อได้ตรงแล้วก็นำโค้ดมาใช้งาน ประหยัดเวลาไปได้มากครับ

/// Converts Cartesian (ECEF) coordinates to latitude, longitude, and height.
fn ecef_to_geodetic(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
    let lon = y.atan2(x).to_degrees(); // Longitude in degrees

    let p = (x.powi(2) + y.powi(2)).sqrt(); // Distance from z-axis
    let mut lat = (z / p).atan(); // Initial guess for latitude

    // Iterative computation for latitude
    loop {
        let sin_lat = lat.sin();
        let n = A / (1.0 - E2 * sin_lat.powi(2)).sqrt();
        let new_lat = (z + E2 * n * sin_lat) / p;
        let new_lat = new_lat.atan();

        if (new_lat - lat).abs() < EPSILON {
            lat = new_lat;
            break;
        }
        lat = new_lat;
    }

    // Final calculation for height
    let sin_lat = lat.sin();
    let n = A / (1.0 - E2 * sin_lat.powi(2)).sqrt();
    let height = p / lat.cos() - n;

    (lat.to_degrees(), lon, height) // Latitude and longitude in degrees, height in meters
}

/// Converts latitude, longitude, and height to Cartesian (ECEF) coordinates.
fn geodetic_to_ecef(lat: f64, lon: f64, height: f64) -> (f64, f64, f64) {
    //println!("Test lat, lon, height: {}, {}, {}", lat, lon, height);
    // Convert latitude and longitude from degrees to radians
    let lat_rad = lat.to_radians();
    let lon_rad = lon.to_radians();

    // Calculate the prime vertical radius of curvature
    let sin_lat = lat_rad.sin();
    let n = A / (1.0 - E2 * sin_lat * sin_lat).sqrt();

    // Compute Cartesian coordinates
    let x = (n + height) * lat_rad.cos() * lon_rad.cos();
    let y = (n + height) * lat_rad.cos() * lon_rad.sin();
    let z = (n * (1.0 - E2) + height) * sin_lat;

    (x, y, z)
}

โค้ดภาษา Slint

การแยกโค้ด GUI มาต่างหาก ผมว่าก็ดีครับเพราะดูแลรักษาง่าย โค้ดของ Slint เขียนง่ายหลังจากดูคลิปยูทูปก็ทำได้เลย ในทางตรงกันข้ามการนำเอา GUI ไปรวมกันกับภาษาหลักเช่น Dart/Flutter ทำให้โค้ดยาวเหยียด ดูแลรักษาลำบาก

ต่อไปเป็นโค้ดภาษา Slint อยู่ในซับโฟลเดอร์ “ui” ชื่อ “app-windows.slint“

import { HorizontalBox, VerticalBox, LineEdit, GridBox, Button, GroupBox } from "std-widgets.slint";

export struct Coordinate3D {
    x: string,
    y: string,
    z: string,
}

export enum CalculateDirection {
    Geo2Ecef,
    Ecef2Geo,
}

export global StatusPalette {
    in-out property <color> normal: rgb(0,255,0);
    in-out property <color> error: rgb(255,0,0);
}

export component MainWindow inherits Window {
    icon: @image-url("icons/world.png");
    title: "GeoEcef";
    in-out property <color> status_color: StatusPalette.normal;
    in property <string> ylat;
    in property <string> xlon;
    in property <string> zhi;
    in-out property <CalculateDirection> calc_dir;
    in property <string> xcart;
    in property <string> ycart;
    in property <string> zcart;
    in property <Coordinate3D> in_geo:{ y: lat.text, x: lon.text, z: hi.text };
    in property <Coordinate3D> in_ecef:{ y: y.text, x: x.text, z: z.text };
    in-out property <string> status;
    callback transform_coordinate(CalculateDirection, Coordinate3D);
    callback erase_latlon();
    callback erase_ecef();
    callback example_latlon();
    callback example_ecef();
    callback copy_latlon();
    callback copy_ecef();
    VerticalLayout {
        GridBox {
            GroupBox {
                title: "Geographic (GNSS) on WGS84";
                VerticalBox {
                    GridBox {
                        spacing: 8px;
                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "Latitude (φ):";
                            }

                            lat := LineEdit {
                                height: 40px;
                                read-only: calc_dir == CalculateDirection.Ecef2Geo ? true : false;
                                font-size: 16px;
                                text: root.ylat;
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/eraser.svg");
                                clicked => {
                                    root.erase_latlon();
                                }
                            }
                        }

                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "Longitude (λ):";
                            }

                            lon := LineEdit {
                                height: 40px;
                                read-only: calc_dir == CalculateDirection.Ecef2Geo ? true : false;
                                font-size: 16px;
                                text: root.xlon;
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/bulb.svg");
                                clicked => {
                                    root.example_latlon();
                                }
                            }
                        }

                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "Height (h):";
                            }

                            hi := LineEdit {
                                height: 40px;
                                read-only: calc_dir == CalculateDirection.Ecef2Geo ? true : false;
                                font-size: 16px;
                                text: root.zhi;
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/copy.svg");
                                clicked => {
                                    root.copy_latlon();
                                }
                            }
                        }
                    }
                }
            }

            VerticalBox {
                spacing: 8px;
                Button {
                    primary: false;
                    width: 60px;
                    height: 40px;
                    icon: calc_dir == CalculateDirection.Geo2Ecef ? @image-url("icons/right_arrow.svg") : @image-url("icons/left_arrow.svg");
                    clicked => {
                        if calc_dir == CalculateDirection.Geo2Ecef {
                            calc_dir = CalculateDirection.Ecef2Geo;
                            status = "Calculate the coordinates from Geocentric to Geographic (GNSS).";
                            status_color = StatusPalette.normal;
                        } else {
                            calc_dir = CalculateDirection.Geo2Ecef;
                            status = "Calculate the coordinates from Geographic (GNSS) to Geocentric.";
                            status_color = StatusPalette.normal;
                        }
                    }
                }

                Button {
                    primary: true;
                    width: 60px;
                    icon: @image-url("icons/calculator.svg");
                    clicked => {
                        if (calc_dir == CalculateDirection.Geo2Ecef) {
                            root.transform_coordinate(calc_dir, in_geo);
                        } else {
                            root.transform_coordinate(calc_dir, in_ecef);
                        }
                    }
                }
            }

            GroupBox {
                title: "XYZ (Geocentric) on WGS84";
                VerticalBox {
                    GridBox {
                        spacing: 6px;
                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "X:";
                            }

                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                x := LineEdit {
                                    read-only: calc_dir == CalculateDirection.Geo2Ecef ? true : false;
                                    font-size: 16px;
                                    text: root.xcart;
                                }
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/eraser.svg");
                                clicked => {
                                    root.erase_ecef();
                                }
                            }
                        }

                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "Y:";
                            }

                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                y := LineEdit {
                                    font-size: 16px;
                                    read-only: calc_dir == CalculateDirection.Geo2Ecef ? true : false;
                                    text: root.ycart;
                                }
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/bulb.svg");
                                clicked => {
                                    root.example_ecef();
                                }
                            }
                        }

                        Row {
                            Text {
                                height: 40px;
                                vertical-alignment: TextVerticalAlignment.center;
                                font-size: 16px;
                                text: "Z:";
                            }

                            Text {
                                height: 40px;
                                z := LineEdit {
                                    font-size: 16px;
                                    read-only: calc_dir == CalculateDirection.Geo2Ecef ? true : false;
                                    text: root.zcart;
                                }
                            }

                            Button {
                                width: 40px;
                                height: 40px;
                                icon: @image-url("icons/copy.svg");
                                clicked => {
                                    root.copy_ecef();
                                }
                            }
                        }
                    }
                }
            }
        }

        HorizontalBox {
            Text {
                vertical-alignment: TextVerticalAlignment.center;
                color: status_color;
                text: root.status;
            }
        }
    }
}

โค้ด Cargo.toml

Crate หรือไลบรารีที่นำมาใช้แน่นอน หนึ่งในนั้นต้องเป็น Slint และมีสองตัวช่วยคือ arboard สำหรับการก๊อปปี้ผลลัพธ์การคำนวณไปคลิปบอร์ด และสุดท้ายคือเครท latlon สำหรับการช่วยแยก (parse) ค่าพิกัดภูมิศาสตร์ที่ได้จากการป้อนมาเป็นทศนิยม

[package]
name = "geoecef"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
arboard = "3.4.1"
latlon = "0.1.3"
slint = "1.8.0"

[build-dependencies]
slint-build = "1.8.0"

โค้ดหลัก (main.rs)

เวลาเราคลิกอะไรใน GUI เช่นปุ่มไอคอนเครื่องคิดเลข จะต้องกำหนดสิ่งที่เรียกว่า callback ให้กับ slint เป็น “root.transform_coordinate” แล้วจะโยงมาที่โค้ดหลักที่ผมกำหนดเป็น “app.on_transform_coordinate” โค้ดตัวนี้จะหุ้มที่เรียกว่า closure เมื่อโปรแกรมรันจะอ่านโค้ดนี้มาทั้งหมดก่อน มีสิ่งที่น่าสนใจคือโค้ดพวกนี้เป็นพวกขี้เกียจ (lazy) เมื่อผู้ใช้คลิกที่ปุ่มตอนรันโปรแกรม โค้ดขี้เกียจเหล่านี้จะถูกปลุกเรียกมาใช้อีกที

// Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

//use latlon::GeoParseError;
use arboard::Clipboard;
use latlon::GeoParseError;
use slint::Color;
use std::{num::ParseFloatError, rc::Rc, sync::Mutex};

// WGS84 constants
const A: f64 = 6378137.0; // Semi-major axis (meters)
const F: f64 = 1.0 / 298.257223563; // Flattening
const E2: f64 = 2.0 * F - F * F; // Square of eccentricity
const EPSILON: f64 = 1e-12; // Convergence tolerance

slint::include_modules!();
fn main() {
    let app = MainWindow::new().unwrap();
    let clipboard = Rc::new(Mutex::new(Clipboard::new().unwrap()));
    let cb1 = clipboard.clone();
    let cb2 = clipboard.clone();

    app.on_erase_latlon({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            ui.set_xlon("".into());
            ui.set_ylat("".into());
            ui.set_zhi("".into());
        }
    });

    app.on_erase_ecef({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            ui.set_xcart("".into());
            ui.set_ycart("".into());
            ui.set_zcart("".into());
        }
    });

    app.on_example_latlon({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            ui.set_ylat("15°13'12.1252\"N".into());
            ui.set_xlon("100 12 12.3256 E".into());
            ui.set_zhi("5.202".into());
        }
    });

    app.on_example_ecef({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            ui.set_xcart("-1070053.3249".into());
            ui.set_ycart("6068573.9676".into());
            ui.set_zcart("1640100.7872".into());
        }
    });

    app.on_copy_latlon({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            let y = ui.get_ylat();
            let x = ui.get_xlon();
            let z = ui.get_zhi();
            let the_string = [y, x, z].join(",");
            cb1.lock().unwrap().set_text(the_string).unwrap();
        }
    });

    app.on_copy_ecef({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move || {
            let x = ui.get_xcart();
            let y = ui.get_ycart();
            let z = ui.get_zcart();
            let the_string = [x, y, z].join(",");
            cb2.lock().unwrap().set_text(the_string).unwrap();
        }
    });

    app.on_transform_coordinate({
        let app_weak = app.as_weak();
        let ui = app_weak.unwrap();
        move |calc_dir: CalculateDirection, in_coor3d: Coordinate3D| {
            let (xx, yy, zz): (f64, f64, f64);
            if calc_dir == CalculateDirection::Ecef2Geo {
                //check validity of input values.
                (xx, yy, zz) = match parse_ecef(&in_coor3d) {
                    Ok(val) => {
                        ui.set_status("".into());
                        (val.0, val.1, val.2)
                    }
                    Err(e) => {
                        ui.set_status_color(Color::from_rgb_u8(255, 00, 0));
                        ui.set_status(format!("Error: {}", e).into());
                        return ();
                    }
                };
            } else {
                (xx, yy) = match parse_latlong(&in_coor3d) {
                    Ok(val) => {
                        ui.set_status("".into());
                        (val.0, val.1)
                    }
                    Err(e) => {
                        ui.set_status_color(Color::from_rgb_u8(255, 00, 0));
                        ui.set_status(format!("{}", e).into());
                        return ();
                    }
                };
                zz = match in_coor3d.z.parse::<f64>() {
                    Err(e) => {
                        ui.set_status_color(Color::from_rgb_u8(255, 00, 0));
                        ui.set_status(format!("Error: {}", e).into());
                        return ();
                    }
                    Ok(val) => {
                        ui.set_status("".into());
                        val
                    }
                };
            }
            println!("parsed latlon xx, yy, zz: {}, {}, {}", xx, yy, zz);
            // Calculate the results
            if calc_dir == CalculateDirection::Geo2Ecef {
                let (x, y, z) = geodetic_to_ecef(yy, xx, zz);
                println!("output x, y, z: {}, {}, {}", x, y, z);
                // Output Ecef coordinates
                ui.set_xcart(format!("{:.4}", x).into());
                ui.set_ycart(format!("{:.4}", y).into());
                ui.set_zcart(format!("{:.4}", z).into());
            } else {
                let (y, x, z) = ecef_to_geodetic(xx, yy, zz);
                println!("output x, y, z: {}, {}, {}", x, y, z);
                // Output geographic coordinates
                ui.set_xlon(format!("{:.8}", x).into());
                ui.set_ylat(format!("{:.8}", y).into());
                ui.set_zhi(format!("{:.4}", z).into());
            }
        }
    });

    app.run().unwrap();
}

fn parse_latlong(coor3d: &Coordinate3D) -> Result<(f64, f64), GeoParseError<&slint::SharedString>> {
    let yy = match latlon::parse_lat(&coor3d.y) {
        Err(e) => return Err(e),
        Ok(val) => val,
    };
    let xx = match latlon::parse_lng(&coor3d.x) {
        Err(e) => return Err(e),
        Ok(val) => val,
    };
    Ok((xx, yy))
}

fn parse_ecef(coor3d: &Coordinate3D) -> Result<(f64, f64, f64), ParseFloatError> {
    //check validity of input values.
    let yy = match coor3d.y.parse::<f64>() {
        Err(e) => {
            return Err(e);
        }
        Ok(val) => val,
    };
    let xx = match coor3d.x.parse::<f64>() {
        Err(e) => {
            return Err(e);
        }
        Ok(val) => val,
    };
    let zz = match coor3d.z.parse::<f64>() {
        Err(e) => {
            //ui.set_status(format!("Error: {}", e).into());
            return Err(e);
        }
        Ok(val) => {
            //ui.set_status("".into());
            val
        }
    };
    Ok((xx, yy, zz))
}

/// Converts Cartesian (ECEF) coordinates to latitude, longitude, and height.
fn ecef_to_geodetic(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
    let lon = y.atan2(x).to_degrees(); // Longitude in degrees

    let p = (x.powi(2) + y.powi(2)).sqrt(); // Distance from z-axis
    let mut lat = (z / p).atan(); // Initial guess for latitude

    // Iterative computation for latitude
    loop {
        let sin_lat = lat.sin();
        let n = A / (1.0 - E2 * sin_lat.powi(2)).sqrt();
        let new_lat = (z + E2 * n * sin_lat) / p;
        let new_lat = new_lat.atan();

        if (new_lat - lat).abs() < EPSILON {
            lat = new_lat;
            break;
        }
        lat = new_lat;
    }

    // Final calculation for height
    let sin_lat = lat.sin();
    let n = A / (1.0 - E2 * sin_lat.powi(2)).sqrt();
    let height = p / lat.cos() - n;

    (lat.to_degrees(), lon, height) // Latitude and longitude in degrees, height in meters
}

/// Converts latitude, longitude, and height to Cartesian (ECEF) coordinates.
fn geodetic_to_ecef(lat: f64, lon: f64, height: f64) -> (f64, f64, f64) {
    //println!("Test lat, lon, height: {}, {}, {}", lat, lon, height);
    // Convert latitude and longitude from degrees to radians
    let lat_rad = lat.to_radians();
    let lon_rad = lon.to_radians();

    // Calculate the prime vertical radius of curvature
    let sin_lat = lat_rad.sin();
    let n = A / (1.0 - E2 * sin_lat * sin_lat).sqrt();

    // Compute Cartesian coordinates
    let x = (n + height) * lat_rad.cos() * lon_rad.cos();
    let y = (n + height) * lat_rad.cos() * lon_rad.sin();
    let z = (n * (1.0 - E2) + height) * sin_lat;

    (x, y, z)
}

ส่งโค้ดขึ้น Github

ผมใช้ VSCode ติดตั้ง extension ของ Slint มาช่วยในการตรวจ syntax ต่างๆ และที่ต้องใช้คือ extension ทางการของ Rust คือ “rust-analyzer” จากนั้นเมื่อโค้ดรันได้น่าพอใจแล้ว ก็ส่งโค้ดเข้า github ใครสนใจก็สามารถไป clone มารันได้

รันโค้ด

การโคลนโค้ดได้ที่ ลิ๊งค์นี้ หรือผ่านคำสั่ง จากนั้นทำการบิวด์และรันได้

PS C:\Users\priab\projects>git clone https://github.com/pbroboto/geoecef
PS C:\Users\priab\projects>cd geoecef
PS C:\Users\priab\projects\geoecef>cargo build
PS C:\Users\priab\projects\geoecef>cargo run

ทดสอบโปรแกรม

เมื่อโปรแกรมรันแล้ว จะเห็นหน้าตาแบบนี้

มาลองดูวิธีการใช้งานคร่าวๆกันดูครับ

ทดสอบได้โดยการคลิกหลอดไฟสีเหลืองที่ด้านซ้าย โปรแกรมจะคัดลอกค่าตัวอย่างที่เตรียมไว้มาใส่ในช่องป้อนข้อมูล

คลิกที่เครื่องคิดเลข แล้วทิศทางการแปลงพิกัดต้องชี้ไปด้านขวา คลิกที่ไอคอนเครื่องคิดเลขเพื่อคำนวณ

ทำการก๊อปปีผลลัพธ์ไปคลิปบอร์ด

ลอง paste ลง Notepad ของวินโดส์

ตัวอย่างการป้อนค่าพิกัด Geocentric ไปยัง Geographic ก็คล้ายๆกัน ต้องการลบตัวเลขก่อนหน้านี้ออกก็คลิกที่ไอคอนยางลบทั้งด้านซ้ายและด้านขวา จากนั้นคลิกที่หลอดไฟด้านซ้าย ตัวอย่างจะขึ้นมาให้ คลิกลูกศรให้ชี้ไปด้านซ้าย จากนั้นคลิกไอคอนเครื่องคิดเลขเพื่อคำนวณ คลิกไอคอน copy ถ้าต้องการลอกผลลัพธ์เข้าคลิปบอร์ด ผมลองไป paste ลง notepad ของวินโดส์ก็ได้ตามรูป

เมื่อทดลองเขียนโปรแกรมภาษา Rust จากที่ศึกษามาหน้าดำคร่ำเครียดก็น่าพอใจ ที่สามารถเขียนโปรแกรมขนาดเล็กได้ โดยที่ไม่ต้องออกแรงมากนัก บางอย่างได้ทวนสิ่งที่ไม่เข้าใจไปพร้อมๆกัน ตอนนี้ผมคิดว่าผมพร้อมที่จะลงมือเขียนโปรแกรมด้วยภาษา Rust ที่มีขนาดใหญ่มากขึ้น โปรดติดตามตอนต่อไปครับ

Related

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Sep    

Blog Stats

There was an issue retrieving stats. Please try again later.

Recent Posts

  • เร็วแรงทะลุนรกกับ POCUT (POint Cloud UTils)
  • อัพเดทแอพ “SurveyStar LDP Thailand” เพื่อรับกับยกร่างมาตรฐาน “TH-LDP”
  • ออกแอพใหม่ SurveyStar Traverse Pro สืบสานงานคำนวณวงรอบ ตอนที่ 2
  • ออกแอพใหม่ SurveyStar Traverse Pro สืบสานงานคำนวณวงรอบ ตอนที่ 1
  • โปรแกรมอรรถประโยชน์ Pocut (POint Cloud Utils)
  • โปรแกรมแปลงไฟล์แคดข้ามระบบพิกัด (Drawing Transformer)
  • มาตรฐานระบบพิกัดแผนที่แบบแอลดีพีของประเทศไทยสำหรับงานวิศวกรรมและการก่อสร้าง (Thailand Low Distortion Map Coordinate System for Engineering and Construction)
  • ประสบการณ์ใช้งาน Kimi K3 Code เขมือบแบบสยอง
  • สร้างแพลตฟอร์มดู Point Cloud บนเว็บด้วย Potree + CesiumJS — เรื่องเล่าจากสนามบินอู่ตะเภา
  • เมื่อวิศวกรสำรวจคำนวณเรื่องน้ำ – วิเคราะห์ระบบระบายน้ำบริเวณ RESA
  • แนะนำหนังสือ “จีออเดซี” ที่ยอดเยี่ยมของดร.พุทธิพล
  • ESPrtk ทำให้ ESP32 ใช้ไม่ได้? วิธีกู้คืน
  • กรณีศึกษา – คืนชีพ SDL30 ปี 2000 ด้วย ESP32 และแอพทำเอง SDL30 Collector
  • DIY RTK Base Station (survey-grade) รุ่น V3.21 ด้วย Raspberry Pi 2B + UM980 เมื่อคุณปู่กลับมาเป็นฮีโร่ ตอนที่ 4
  • รีวิวกูปรี (CoPre2) โปรแกรมประมวลผลไลดาร์จากค่าย CHCNav
  • DIY RTK Base Station (survey-grade) รุ่น V3.2 ด้วย Orange Pi 3 Zero + UM980 ตอนที่ 3
  • DIY RTK Base Station (survey-grade) รุ่น V3 ด้วย Orange Pi 3 Zero + UM980 ตอนที่ 2
  • เส้นทางและไทม์ไลน์ของใบรับรองความรู้ความชำนาญในการประกอบอาชีพวิศวกรรม(สำรวจ)ระดับวิศวกรวิชาชีพ
  • มัดแหนมรวมสามโปรแกรมไปใช้บนสโตร์ Snap ฝั่งลีนุกซ์
  • ปัดฝุ่น Traverse Pro เพิ่มรายละเอียดรายการคำนวณใหม่แบบแนวนอน (Landscape) และธีมมืด
  • DIY RTK Base Station (Survey-Grade) ด้วยชิป UM980 และบอร์ด ESP32 ตอนที่ 1
  • สร้าง NTRIP Caster ด้วย BNC บนคลาวด์แบบ DIY สำหรับ RTK Base Station ใช้ CHCNav P5
  • จับ AI มาไตรกรีฬา สร้างตัวติดตั้ง Snap สำหรับ Surveyor Pocket Tools กับประเด็นแฝงเร้นความเป็นส่วนตัวของผู้ใช้
  • ทดสอบรีด QGIS แบบโหดสัสด้วยชุดข้อมูล Open Building V3 โพลีกอน 74 ล้านหลัง
  • เมื่อ AI เป็นพิษชักจูงผู้ใช้ด้วยข้อมูลผิดๆ

Subscribe to Blog via Email

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 122 other subscribers

Archives

  • October 2026
  • September 2026
  • August 2026
  • July 2026
  • May 2026
  • April 2026
  • March 2026
  • January 2026
  • December 2025
  • November 2025
  • September 2025
  • August 2025
  • July 2025
  • June 2025
  • May 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • July 2024
  • May 2024
  • April 2024
  • January 2024
  • November 2023
  • October 2023
  • September 2023
  • July 2023
  • June 2023
  • May 2023
  • March 2023
  • February 2023
  • January 2023
  • November 2022
  • October 2022
  • September 2022
  • July 2022
  • May 2022
  • December 2021
  • October 2021
  • September 2021
  • August 2021
  • May 2021
  • February 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • July 2020
  • June 2020
  • May 2020
  • April 2020
  • March 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • May 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • January 2018
  • November 2017
  • July 2017
  • June 2017
  • May 2017
  • April 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • August 2016
  • November 2015
  • March 2015
  • August 2014
  • March 2014
  • January 2014
  • December 2013
  • February 2012
  • January 2012
  • November 2011
  • October 2011
  • August 2011
  • April 2011
  • February 2011
  • December 2010
  • November 2010
  • October 2010
  • September 2010
  • June 2010
  • April 2010
  • March 2010
  • January 2010
  • December 2009
  • November 2009
  • October 2009
  • September 2009
  • August 2009

Recent Comments

  • priabroy on ออกแอพใหม่ SurveyStar Traverse Pro สืบสานงานคำนวณวงรอบ ตอนที่ 1
  • Konexana Yomleuangsa on ออกแอพใหม่ SurveyStar Traverse Pro สืบสานงานคำนวณวงรอบ ตอนที่ 1
  • เฟือง เถา on DIY RTK Base Station (survey-grade) รุ่น V3.21 ด้วย Raspberry Pi 2B + UM980 เมื่อคุณปู่กลับมาเป็นฮีโร่ ตอนที่ 4
  • priabroy on DIY RTK Base Station (survey-grade) รุ่น V3.21 ด้วย Raspberry Pi 2B + UM980 เมื่อคุณปู่กลับมาเป็นฮีโร่ ตอนที่ 4
  • ตรันฟองนาม on DIY RTK Base Station (survey-grade) รุ่น V3.21 ด้วย Raspberry Pi 2B + UM980 เมื่อคุณปู่กลับมาเป็นฮีโร่ ตอนที่ 4
©2026 Prajuab Riabroy's Blog | Design: Newspaperly WordPress Theme