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

หลังจากที่ได้ศึกษาภาษา 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 ที่มีขนาดใหญ่มากขึ้น โปรดติดตามตอนต่อไปครับ

Leave a Reply

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