Tag: Geographic

Update: โปรแกรมแปลงค่าพิกัดภูมิศาสตร์ Geographic Calculator (GeoCalc) บนเครื่องคิดเลข Casio fx-9860G II SD

Geographic Calculator

สืบเนื่องจากตอนก่อนหน้านี้ผมได้นำเสนอโปรแกรมแปลงพิกัด Geographic Calculator แบบไม่ได้ใช้ไลบรารีช่วยเรื่อง User Interface โปรแกรมมีลักษณะง่ายๆ เปิดมาเจอเมนูเลือกลักษณะที่จะคำนวณ จากนั้นโปรแกรมจะถามค่าพิกัดที่ต้องการแปลงแล้วคำนวณให้ ข้อดีคือใช้ง่าย ข้อเสียถ้าป้อนข้อมูลผิดพลาด จะย้อนกลับไม่ได้ ต้องเดินหน้าผิดไปจนจบ แล้วค่อยย้อนกลับมาอีกที

เปลี่ยนรูปแบบการติดต่อกับผู้ใช้ด้วยไลบรารี MyLib

ไลบรารี MyLib เป็นไลบรารีภาษาซีเล็กๆที่ผู้พัฒนาใช้นาม hayzel ได้เขียนไว้เพื่อใช้บนเครื่องคิดเลข Casio fx-9860G II SD ผมนำมาใช้และชอบ ทำให้มีแรงใจที่จะเขียนโปรแกรมบนเครื่องคิดเลขรุ่นเทพรุ่นนี้ได้มาหลายโปรแกรม และก็เหมือนเดิมครับว่าโปรแกรมที่ผมเขียนนั้นใช้งานได้ฟรี (Freely Usability) เพื่อใช้ในแวดวงงานสำรวจทำแผนที่ตลอดจนงานสำรวจเพื่อการก่อสร้างก็ตาม

ก็ขอตั้งชื่อโปรแกรมเล็กๆสำหรับแปลงพิกัดบนพื้นหลักฐาน WGS84 นี้ว่า “GeoCalc Extra” ก่อนอื่นสูตรที่ใช้ในการคำนวณผมใช้ไลบรารีชื่อ mgrs สามารถคำนวณแปลงพิกัดในระบบพิกัด UTM, geographic, UPS และ MGRS ได้ ขนาดไม่ใหญ่มากนัก สามารถคอมไพล์และบิวท์มาใส่เครื่องคิดเลขรุ่นนี้ได้ ทั้ง MyLib และ mgrs เป็นโปรแกรมเปิดโค้ด ฟรีทั้งคู่

ดาวน์โหลดโปรแกรม (Download)

ไปที่หน้าดาวน์โหลด (Download) มองหาโปรแกรมบนเครื่องคิดเลข Casio fx-9860G II SD  ชื่อโปรแกรม GeoCalc Extra จากนั้นทำการดาวน์โหลดมาจะได้ไฟล์ชื่อ “GEOCALC.G1A” แล้วทำการ copy ไฟล์ตัวนี้ไปยังเครื่องคิดเลขด้วยโปรแกรม Casio FA-124  หรือ copy ผ่านทางตัว SD Card ที่มากับเครื่องคิดเลข

เริ่มใช้โปรแกรม

ที่ “Main Menu” ของเครื่องคิดเลขเลื่อนไปหาไอคอนของโปรแกรมดังรูปด้านล่าง เมื่อกดคีย์ “EXE” แล้วจะเข้าเมนูของโปรแกรม ดังนี้

F1 – Set เลือกรายการคำนวณแปลงพิกัดระหว่าง UTM, Geographic หรือ MGRS

F2 – Src (Source) ป้อนค่าพิกัดที่ต้องการแปลงพิกัด

F3 – Calc คำนวณแปลงพิกัดพร้อมแสดงผลลัพธ์

F5 – Info แสดงเครดิตไลบรารีที่โปรแกรมใช้งาน

F6 – Exit ออกจากโปรแกรม

เลือกรายการคำนวณ (Menu)

ที่เมนูหลักกดคีย์ F1 – Set เพื่อเข้าไปเลือกรายการคำนวณ จะเห็น ระบบพิกัดเริ่มต้น (Source)   และระบบพิกัดปลายทาง (Target) ส่วนด้านล่าง MGRS Precision จะเป็นความละเอียดของระบบพิกัด MGRS (Military Grid Reference System) เลือกได้ 6 ระดับคือ 0, 2, 4, 6, 8, 10

ตัวอย่างที่ 1 แปลงค่าพิกัดจากค่าพิกัดภูมิศาสตร์ไปยังค่าพิกัดยูทีเอ็ม (Geographic to UTM)

ตั้งค่าระบบพิกัดเริ่มต้นและปลายทาง

ตั้งค่าระบบพิกัดต้นทางและปลายทางดังรูป จากนั้นกดคีย์ F6 – OK เพื่อออก

ป้อนค่าพิกัด

กลับมาที่เมนูหลักของโปรแกรมอีกครั้ง กดคีย์ F2 – Src เพื่อป้อนค่าพิกัดภูมิศาสตร์

ป้อนค่าพิกัดละติจูด Latitude 39°57’9.34803″N โดยการป้อน 39-57-9.34803N ค่าลองจิจูด 75°9’54.75490″W ป้อนค่า 75-9-54.75490W เสร็จแล้วกดคีย์ F6 – OK เพื่อออกไปคำนวณ

คำนวณแปลงพิกัด

กลับมาที่เมนูหลักของโปรแกรม กดคีย์ F3 – Calc เพื่อคำนวณจะได้ผลลัพธ์ดังรูปด้านล่าง โปรแกรม

จะแสดงค่าพิกัดเริ่มต้นให้และค่าพิกัดปลายทางคือยูทีเอ็มให้ พร้อมทั้งบอกโซนของยูทีเอ็มให้ กดคีย์ F6 – Done เพื่อออก

ตัวอย่างที่ 2 แปลงค่าพิกัดจากค่าพิกัดยูทีเอ็มไปยังค่าพิกัด MGRS (UTM to MGRS)

กำหนดค่าพิกัดยูทีเอ็ม (UTM) ดังนี้ N: 2642783.110, E: 232030.949 UTM Zone No: 46N กลับมาที่เมนูหลักของโปรแกรม กดคีย์ F1 – Set เพื่อเปลี่ยนรายการคำนวณ ตั้งค่าตามรูปด้านล่าง 

เสร็จแล้วกดคีย์ F6 – OK เพื่อออก

กลับมาเมนูหลักของโปรแกรมกดีย์ F2 – Src เพื่อออกป้อนค่าพิกัดยูทีเอ็มดังนี้ จากนั้นกดคีย์ F6 – OK เพื่อออก

กลับมาที่เมนูหลักของโปรแกรม กดคีย์ F3 – Calc เพื่อคำนวณ จะได้ผลลัพธ์ดังรูปด้านล่าง

ตัวอย่างที่ 3 แปลงค่าพิกัดจากค่าพิกัด MGRS ไปยังค่าพิกัดภูมิศาสตร์ (MGRS to Geographic)

กำหนดค่าพิกัด MGRS: 46QCK0907425049 ส่วนขั้นตอนจะขอรวบรัดแสดงด้วยรูปภาพ

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

แนะนำโปรแกรมมิ่งภาษาซีบนเครื่องคิดเลข Casio fx-9860G II SD ด้วยเครื่องมือพัฒนา SDK ของ Casio

แนะนำโปรแกรมมิ่งภาษาซีบนเครื่องคิดเลข Casio fx-9860G II SD ด้วยเครื่องมือพัฒนา SDK ของ Casio

เคยเกริ่นมาก่อนว่าต้องการเขียนบทความนี้ขึ้นมาเพื่อวงการศึกษาบ้านเราที่สนใจเรื่องโปรแกรมมิ่งบนเครื่องคิดเลขสามารถจะพัฒนาโปรแกรมภาษาซีบน Casio fx-9860G II SD หรือรุ่นที่ใกล้เคียงนี้ได้ โดยที่มีไม่มีข้อจำกัดด้านภาษาโปรแกรมมิ่ง เหมือนกับภาษา casio basic อาจจะส่งผลให้ในอนาคต มีโปรแกรมที่พัฒนาโดยบุคคลากรท่านอื่นๆ เข้ามาสู่วงการนี้มากขึ้น และได้ตัวโปรแกรมงานสำรวจที่มีความหลากหลายและความสามารถมากขึ้นทั้งนี้เพื่อขยายขีดความสามารถโปรแกรมบนเครื่องคิดเลขให้สามารถคิดงานที่ยาก ซับซ้อนได้ บางครั้งเกือบจะเทียบเท่าโปรแกรมที่ใช้งานบนคอมพิวเตอร์

เครื่องมือพัฒนา Software Development Kit (SDK)

เครื่องมือตัวนี้เดิมทีสามารถดาวน์โหลดที่เว็บไซต์ตามลิ๊งค์นี้ได้ http://edu.casio.com/support/en/agreement.html#2 ขั้นตอนแรกยอมรับเงื่อนไขแล้วเลื่อนหน้าไปด้านล่างๆจะเห็นเครื่องคิดเลขรุ่น fx-9860 เมื่อคลิกลิ๊งค์ SDK เข้าไปจะเห็นว่าลิ๊งค์เครื่องมือพัฒนาโปรแกรมขาด แต่คู่มือยังสามารถดาวน์โหลดมาอ่านศึกษาได้ ผมอาศัยลงใต้ดินที่มีคนปล่อยให้ดาวน์โหลด (ถ้าใครอยากได้เครื่องมือตัวนี้ก็ขอมาหลังไมค์กันได้ครับ) และดาวน์โหลดโปรแกรม FA-124 มาด้วยอยู่ในหมวด Support Software/PC Link software

ติดตั้งเครื่องมือพัฒนาโปรแกรม

เปิดไฟล์ zip ของเครื่องมือพัฒนาจะเห็นไฟล์ข้างในดังนี้

จากนั้นทำการติดตั้งเครื่องมือลงคอมพิวเตอร์  ข้อสำคัญคือพาทของโฟลเดอร์หรือไดเรคทอรีที่ติดตั้งจะต้องไม่มีช่องว่าง ดังนั้นให้ติดตั้งไปที่รากของไดรว์ C: ตัวอย่างผมใช้ชื่อว่า fx-9860-sdk 

ถ้าพาทของโฟลเดอร์ที่ติดตั้งมีช่องว่างการ compile & build จะไม่ผ่านเลย เมื่อติดตั้งแล้วจะมีไอคอนที่หน้า desktop

เริ่มต้นใช้งาน

เมื่อเปิดโปรแกรมจากไอคอนที่ desktop จะเห็นหน้าตาเครื่องมือพัฒนาโปรแกรม ดังรูป อาจจะดูทื่อๆเพราะเครื่องมือตัวนี้ออกมานานแล้วตั้งแต่วินโดส์รุ่นก่อนหน้านี้ ที่เมนู “Project” คลิกเลือก “New” ผมสร้างโฟลเดอร์ชื่อ “FX9860GIISD” ไว้ที่ไดรว์ D: และตั้งชื่อโฟลเดอร์สำหรับทดสอบการเขียนโปรแกรมนี้ว่า “Test” และตั้งชื่อโปรแกรมว่า “Hello”

จะเห็นหน้าตาเครื่องมือพัฒนาประมาณรูปด้านล่าง และตัวอีมูเลเตอร์ “Display” และ “Keyboard” ของ fx-9860G

Project แรกเริ่ม

เมื่อเราสร้าง Project ใหม่จะเห็นโครงร่างที่เครื่องมือเขียนมาให้ดังนี้

/*****************************************************************/
/*                                                               */
/*   CASIO fx-9860G SDK Library                                  */
/*                                                               */
/*   File name : Hello.c                                 */
/*                                                               */
/*   Copyright (c) 2006 CASIO COMPUTER CO., LTD.                 */
/*                                                               */
/*****************************************************************/
#include "fxlib.h"

//****************************************************************************
//  AddIn_main (Sample program main function)
//
//  param   :   isAppli   : 1 = This application is launched by MAIN MENU.
//                        : 0 = This application is launched by a strip in eACT application.
//
//              OptionNum : Strip number (0~3)
//                         (This parameter is only used when isAppli parameter is 0.)
//
//  retval  :   1 = No error / 0 = Error
//
//****************************************************************************
int AddIn_main(int isAppli, unsigned short OptionNum)
{
    unsigned int key;

    Bdisp_AllClr_DDVRAM();

    locate(1,4);
    Print((unsigned char*)"This application is");
    locate(1,5);
    Print((unsigned char*)" sample Add-In.");

    while(1){
        GetKey(&key);
    }

    return 1;
}

//****************************************************************************
//**************                                              ****************
//**************                 Notice!                      ****************
//**************                                              ****************
//**************  Please do not change the following source.  ****************
//**************                                              ****************
//****************************************************************************

#pragma section _BR_Size
unsigned long BR_Size;
#pragma section

#pragma section _TOP

//****************************************************************************
//  InitializeSystem
//
//  param   :   isAppli   : 1 = Application / 0 = eActivity
//              OptionNum : Option Number (only eActivity)
//
//  retval  :   1 = No error / 0 = Error
//
//****************************************************************************
int InitializeSystem(int isAppli, unsigned short OptionNum)
{
    return INIT_ADDIN_APPLICATION(isAppli, OptionNum);
}
#pragma section

จากโค้ดด้านบนจะเห็นว่าไม่เห็นจุดเริ่มต้นเข้าโปรแกรม (entry point) ฟังก์ชัน main() เช่นภาษาซีทั่วๆไป แต่จะมี AddIn_main() มาแทนให้รู้ว่าเป็นจุดเริ่มต้นของโปรแกรม AddIn สำหรับเครื่องคิดเลขนี้

มาดูโค้ดกันสักนิด ที่ #inlucde “fxlib.h” จะเป็น header ของ Casio สำหรับการแสดงผลเช่นฟังก์ชัน Print เพื่อแสดงบนจอภาพเครื่องคิดเลข Bdisp_AllClr_DDVRAM(); จะเป็นฟังก์ชันเคลียร์หน้าจอภาพให้ว่างเปล่า  locate(1,4); เลื่อนเคอเซอร์มาที่คอลัมน์ 1 และบรรทัดที่ 4 จากนั้นพิมพ์ด้วยฟังก์ชัน Print((unsigned char*)”This application is”); สุดท้ายปิดด้วย loop ไม่รู้จบ while (1) แล้วรอผู้ใช้กดคีย์ GetKey(&key); ตอนรันโปรแกรมถ้าผู้ใช้กดคีย์ “MENU” บนเครื่องคิดเลขก็จะเข้าสู่โหมด “MAIN MENU” แต่โปรแกรมก็ยังรันค้างอยู่ในสถานะเดิม

Compile & Build

มาลองคอมไพล์และบิวด์ดู ที่เมนู “Project” คลิก “Rebuild all” ถ้าไม่มีอะไรผิดพลาดที่กรอบ “Builds” จะแสดงผลดังนี้

Executing Hitachi SH C/C++ Compiler/Assembler phase

set SHC_INC=C:\fx-9860G-SDK\OS\SH\include
set PATH=C:\fx-9860G-SDK\OS\SH\bin
set SHC_LIB=C:\fx-9860G-SDK\OS\SH\bin
set SHC_TMP=D:\FX9860GIISD\Test\Debug
"C:\fx-9860G-SDK\OS\SH\bin\shc.exe" -subcommand=C:\Users\priabroy\AppData\Local\Temp\hmk9A12.tmp

Executing Hitachi OptLinker04 phase

"C:\fx-9860G-SDK\OS\SH\bin\Optlnk.exe" -subcommand=C:\Users\priabroy\AppData\Local\Temp\hmk9D5F.tmp

Optimizing Linkage Editor Completed

HMAKE MAKE UTILITY Ver. 1.1
Copyright (C) Hitachi Micro Systems Europe Ltd. 1998
Copyright (C) Hitachi Ltd. 1998 


	Make proccess completed

"D:\FX9860GIISD\Test\HELLO.G1A" was created.

Build has completed.

ถ้าสำเร็จจะเห็น HELLO.G1A ถูกสร้างขึ้นมา จากนั้นที่เมนู “Run” คลิกที่เมนูย่อย “Run” อีมูเลเตอร์จะเริ่มทำงาน

ที่ “Keyboard” กดปุ่มเลื่อนลูกศรลงที่ไอคอน “Debug” กดคีย์ “EXE” จะเห็นหน้าตาโปรแกรมดังนี้

ดาวน์โหลดซอร์สโค้ด (Source code) โปรแกรมแปลงพิกัดภูมิศาสตร์ (Geographic Calc)

เพื่อการลดระยะเวลาการเรียนรู้สำหรับคนที่เพิ่งจะมาศึกษาการใช้งานเครื่องมือ SDK ผมจะขอแสดงโครงการที่ผมทำไว้แล้ว ไปที่หน้าดาวน์โหลด (Download) มองหาซอร์สโค้ด (Source code) โปรแกรมสำหรับเครื่องคิดเลข Casio fx-9860G II SD จะได้ไฟล์ zip ชื่อ “UTM-Geo.zip” ทำการแตกไฟล์ zip จะเห็นไฟล์ดังนี้

ที่ผมวงสีแดงไว้คือโค้ดที่ได้จาก ไลบรารีที่ผมดาวน์โหลดมาใช้จาก githubพัฒนาโดย Howard Butler ส่วนที่ผมวงสีฟ้าไว้คือโค๊ดของผมเอง แตกไฟล์ zip ไปไว้ที่โฟลเดอร์สำหรับผมเองอยู่ที่ไดรว์ D:\FX9860GIISD

เปิดโครงการโปรแกรมแปลงพิกัดภูมิศาสตร์

ใช้เมนู “Project” > “Open…” เปิดไฟล์ชื่อ “UTMGeo.g1w”  ที่ช่อง panel  ด้านซ้ายสุดของเครื่องมือพัฒนาจะเห็นเป็นชื่อไฟล์ที่ผมได้ add มาไว้ จะสังเกตเห็นชื่อไฟล์บางตัวมี นามสกุล extension *.h, *.c ที่เป็นปกติของไฟล header และซอร์สโค้ดของภาษาซีและ *.hpp, *.cpp ของภาษาซีพลัสพลัส ซึ่งไฟล์ extension ที่เราตั้งชื่อไว้ถ้ามีโค้ดภาษาซีพลัสพลัส จะต้องใช้ extension เป็น hpp และ cpp

แก้ไขโครงการ

ใช้เมนู “Project” > “Edit…” เพื่อแก้ไขชื่อโครงการ หรือเพิ่มลดไฟล์ header และ source file

แก้ไขไอคอนสำหรับปรากฎที่หน้า “MAIN MENU” ของเครื่องคิดเลข ไอคอนขนาดกว้าง 39 pixel และสูงขนาด 19 pixel ฟอร์แม็ตเป็น bitmap แบบขาวดำ 1 bit ผมออกแบบในโปรแกรม Paint.net จัดเก็บเป็นไฟล์ bitmap (bmp) แต่ไม่สามารถจัดเก็บเป็นไฟล์ขาวดำแบบ 1 bit ได้ต้องไปเปิดต่อใน Gimp แล้วเลือกเมนู Image>Mode>Indexed ตรง Color map เลือก Use black and white (1 bit) palette จากนั้น save จึงจะได้ไฟล์รูปที่มีฟอร์แม็ต bitmap แบบ 1 bit ได้ ทั้งสองโปรแกรมนี้ฟรี

ตรง Edit icon เวลาจะคลิกต้องระวังเพราะถ้าเราออกแบบไอคอนมาแล้ว จะโดนทับด้วยรูปไอคอนดีฟอลท์ทันที ผมไม่ใช้เลยเพราะพลาดหลายทีแล้ว

การจัดการโค้ด C++

ถ้ามีโค้ดซีพลัสพลัสมาผสมด้วย ที่ด้านบนสุดไฟล์จะต้องกำหนดดังนี้

#ifdef __cplusplus
  extern "C" {
#endif

ด้านล่างสุดปิดท้ายด้วย

  #ifdef __cplusplus
}
  #endif

โค้ดจัดการการป้อนข้อมูล

การป้อนข้อมูลของงานสำรวจในเครื่องคิดเลขส่วนใหญ่จะเป็นเลขทศนิยมเช่นระยะทางหรือค่าพิกัด แต่ถ้าเป็นมุมทีแยกองศา ลิปดาและฟิลิปดา ปกติในเครื่องคิดเลขเช่น  fx-4500, fx-5800P จะมีคีย์ให้กดสะดวก แต่เครื่อง  fx-9860G II SD กลับเอาไปไว้ลึกมากต้องกดหลายครั้งจากคีย์ “OPTN” แต่ที่ผมช็อคคือใน SDK กลับไม่มีฟังก์ชันให้เรียกใช้งานได้เลย จะต้องเขียนฟังก์ชันขึ้นมาเองทั้งหมด ผมอาศัยไปอ่านตามฟอรั่มที่มีคนแฮ็คไว้ พบว่าสามารถเรียกใช้ฟังก์ชัน EditExpress ที่ทาง Casio ไม่ได้เปิดเผยเอกสารไว้ (สังเกตว่าใช้เป็น function pointer อ่านรายละเอียดการใช้งานได้ที่ลิ๊งค์นี้)

#define SCA 0xD201D002
#define SCB 0x422B0009
#define SCE 0x80010070

const unsigned int sc08DB[] = {SCA, SCB, SCE, 0x08DB};
typedef int(*sc_EE)(int, short, int, char*, char*, short, char*, int );
#define EditExpression (*(sc_EE)sc08DB) 

วิธีใช้งานก็ประมาณนี้

#define MAXEDITBUFFER 21
//
void AddIn_main(int isAppli, unsigned short OptionNum){
int key;
char vBCD[24];
unsigned char sBCD[MAXEDITBUFFER];

   memset( vBCD, 0, sizeof(vBCD));
   memset( sBCD, 0, sizeof(sBCD));
   key = EditExpression(0, KEY_CTRL_RIGHT, 1, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "Input:", 0x04);
   
   locate(1, 4);
   PrintLine(sBCD, 21);
   
   GetKey(&key);
   return 1;
}

ข้อมูลที่ผู้ใช้ป้อนจะกลับมาที่ข้อมูลสตริง sBCD ผมพบว่าเรียกใช้ EditExpression มี  mode ให้ป้อนเลือกว่าจะป้อนข้อมูลเป็น double ไหม ซึ่งตอนป้อนข้อมูลจะรับแค่ตัวเลข แต่ในโหมดตามตัวอย่างด้านบน (พารามิเตอร์สุดท้าย 0x04) นั้นจะรับค่าทั่วๆไปทั้งตัวอักษรผสมกับตัวเลข แต่ผมสังเกตว่าเวลาเราป้อนข้อมูลแล้วกดคีย์ “EXE” จะมีการประมวลผล expression ด้วย ปัญหาที่ผมพบคือถ้าป้อนสตริงค่าพิกัดแบบ MGRS เช่น “18SVK8588822509” กลับ error ผมเลยต้องเขียนฟังก์ชันขึ้นมาเองคือ inputMGRSString() โดยเฉพาะ

การป้อนข้อมูลมุม

เนื่องจากเครื่องมือพัฒนา SDK ไม่สนับสนุนการป้อนมุมแบบใช้งานปกติ ผมเลยกำหนดว่ามุมองศา ลิปดาและฟิลิปดาให้คั่นด้วยเครื่องหมายลบ (-) เช่นแลตติจูด (Latitude) จะใช้ตัวอักษร “N” หรือ “S” มากำกับว่าอยู่ในซึกโลกเหนือหรือซีกโลกใต้ คั่นด้วยเส้นศูนย์สูตร ส่วนมุมลองจิจูด (Longitude) แบ่งเป็นซีกโลกตะวันออก (“E” ปิดท้าย) และซีกโลกตะวันตก (“W” ปิดท้าย

ตัวอย่าง แลตติจูด 45-5-32.525N ลองจิจูด 98-45-38.587W

โค้ดอ่านเขียนข้อมูลเข้าตัวแปร  Alpha

ส่วนใหญ่เวลาเราป้อนข้อมูลเข้าไปในการคำนวณงาน เราต้องการให้โปรแกรมจดจำค่านั้นไว้ ดังนั้นโปรแกรมต้องมีการจัดเก็บเอาไว้แล้วเรียกมาใช้ทีหลังเมื่อเรียกโปรแกรมมาใช้งานอีกที เพราะไม่ต้องป้อนบ่อย ถ้ายังใช้ค่าเดิมแค่กดคีย์ “EXE” ผ่านได้เลย และก็เหมือนเดิมครับ SDK ไม่ได้เปิดเผยฟังก์ชันนี้ไว้ทั้งที่สำคัญมาก ผมไปค้นหาตามฟอรั่มพบว่าการจัดเก็บข้อมูลเข้าตัวแปรตัวอักษร A-Z ใช้โครงสร้างข้อมูลเฉพาะ อ่านได้ตามลิ๊งค์นี้ การแปลงข้อมูลสามารถใช้ class TBCD (โค๊ดเดิมมีบั๊กผมแก้ไปนิดหน่อย)

#define SCA 0xD201D002
#define SCB 0x422B0009
#define SCE 0x80010070

const unsigned int sc04E0[] = {SCA, SCB, SCE, 0x04E0};
const unsigned int sc04DF[] = {SCA, SCB, SCE, 0x04DF};

typedef void(*sc_agd)(char, TBCDvalue*);
typedef void(*sc_asd)(char, TBCDvalue*);

#define Alpha_GetData (*(sc_agd)sc04DF)
#define Alpha_SetData (*(sc_asd)sc04E0)

void GetAlphaDoubleData(char alpha, double *dval);
void SetAlphaDoubleData(char alpha, double val);

typedef struct{
  unsigned char hnibble:4;
  unsigned char lnibble:4;
} TBCDbyte;

typedef struct{
  unsigned short exponent:12;
  unsigned short mantissa0:4;
  TBCDbyte mantissa[7];
  char flags;
  short info;
} TBCDvalue;

typedef struct{
  int exponent;
  int sign;
  int unknown; 
  char mantissa[15];
} TBCDInternal;

//Implement of class TBCD please see utilities.cpp
class TBCD{
  public:
        TBCDvalue*PValue();
        int Get( TBCDvalue&value );
        int Set( TBCDvalue&value );
        int Set( double&value );
        int Get( double&value );
        int SetError( int error );
        int GetError();
        void Swap();
  protected:
  private:
        TBCDvalue FValue[2];
};

void GetAlphaDoubleData(char alpha, double *dval){
  TBCD *bcd;
  TBCDvalue *bval;
  int i, ii;

  bval = (TBCDvalue *)malloc(sizeof(TBCDvalue));
  Alpha_GetData(alpha, bval);
  bcd = new TBCD;
  i = bcd->Set(*bval);
  ii = bcd->Get(*dval);
  delete bcd;
  free(bval);
}

void SetAlphaDoubleData(char alpha, double dval){
  TBCD *bcd;
  TBCDvalue bval;
  int i, ii;

  bcd = new TBCD;
  i = bcd->Set(dval);
  ii = bcd->Get(bval);
  Alpha_SetData(alpha, &bval);
  delete bcd;
}

วิธีการใช้งานก็ง่ายๆ

      SetAlphaDoubleData('B', 123.456); //เอาค่า 123.456 เข้าเก็บที่ตัวแปรอักษร "B"
      GetAlphaDoubleData('B', &x); //ดึงค่าที่เก็บในตัวอักษร "B" ออกมาเข้าตัวแปร x
      Locate(1, 4);
      sprintf((char*) str, (char*) "Value = %.3lf", x); 
      Print((unsigned char*)str); //Value = 123.456     

โค้ดสำหรับเมนูหลัก

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

โค้ดก็ง่ายๆดังนี้

      memset(s, '-', 21);
      Bdisp_AllClr_DDVRAM(); 
      locate(0, 1);
      Print((unsigned char *)"Geographic Calc");
      locate(0, 2);
      PrintLine((unsigned char*)s, 21);
      locate(0, 3);
      PrintLine("[1]:UTM to Geo", 21);
      locate(0, 4);
      PrintLine("[2]:Geo to UTM", 21);
      locate(1, 5);
      PrintLine("[3]:MGRS to Geo", 21);
      locate(0, 6);
      PrintLine("[4]:Geo to MGRS", 21);
      locate(1, 8);
      PrintLine("Select 1,2,3 or 4", 21);
      while (!((key1 >= 0x31) && (key1 <= 0x34)){ 
        GetKey(&key1);

จะมีลูป while ดักการกดคีย์อีก loop ถ้าพบว่ากดคีย์เลข “1” (character code = 0x31) ถึงเลข “4” (Character code = 0x34) เงื่อนไขจริงจะออกจาก loop เข้าเงื่อนไข if (มีหลายชั้นใช้ case แทนได้)

โค้ดงานคำนวณหลัก

ถ้าผู้ใช้กดคีย์ “1” จะเป็นการคำนวณค่าพิกัดจาก UTM ไปยัง Geographic

    if (key1 == 0x31) { //UTM to Geo
      Bdisp_AllClr_DDVRAM(); 
      locate(0, 1);
      Print((unsigned char *)"UTM to Geo");
      locate(0, 2);
      PrintLine((unsigned char*)s, 21);

      GetAlphaDoubleData('A', A);//ดึงค่าข้อมูลเดิมจากหน่วยความจำตัวอักษร "A" 
      sprintf((char*)sBCD, (char*) "%.3lf", *A); //เตรียมรูปแบบข้อมูลทศนิยมสามตำแหน่งไว้ใน sBCD
      //เรียกฟังก์ชันป้อนข้อมูล โดยส่ง sBCD ไปให้
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 3, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "N? : ", 0x04);
      y = atof((char*)sBCD); //แปลงข้อมูลที่ป้อนมาเป็นตัวเลขทศนิยม
      SetAlphaDoubleData('A', y);//เก็บค่าที่ป้อนไว้ในหน่วยความจำตัวอักษร "A"

      GetAlphaDoubleData('B', B);
      sprintf((char*)sBCD, (char*) "%.3lf", *B); 
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 4, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "E? : ", 0x04);
      x = atof((char*)sBCD);
      SetAlphaDoubleData('B', x);
     
      GetAlphaDoubleData('Z', Z);
      sprintf((char*)sBCD, (char*) "%.3lf", *Z); 
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 5, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "UTM Zone No.? : ", 0x04);
      if (strchr((char*)sBCD, 0x87) != NULL) {
        removechar((char*)sBCD, 0x87);
        hemi = 'S';
      } else if(strchr((char*)sBCD, 0x2D) != NULL) {
        removechar((char*)sBCD, 0x2D);
        hemi = 'S';
      } else
        hemi = 'N';

      zn = atoi((char*)sBCD);
      SetAlphaDoubleData('Z', zn);
      //เรียกไลบรารีแปลงพิกัดที่ประกาศใน utm.c
      err = Convert_UTM_To_Geodetic(zn, hemi, x, y, &lat, &lng);
      if (!err){
	      Lat = lat * RAD2DEG; 
	      Lng = lng * RAD2DEG;
              //แยกทศนิยมจัดรูปแบบที่ที่องศา ลิปดาและฟิลิปดาคั่นด้วยเครื่องหมายลบ
	      slat = degreetodms(fabs(Lat), NUMDECIMAL, 0x2D);
	      if(Lat >= 0)
	        sprintf(str, "Lat= %s N", slat);
	      else
	        sprintf(str, "Lat= %s S", slat);
	      locate(1, 6);
	      Print((unsigned char*)str); 
	      locate(1, 7);
	      slong = degreetodms(fabs(Lng), NUMDECIMAL, 0x2D);
	      if (Lng >= 0)
	        sprintf(str, "Lon= %s E", slong);
	      else
	        sprintf(str, "Lon= %s W", slong);
	      Print((unsigned char*)str);      
	      free(slat);
	      free(slong);
       }

ลองดูโค้ดแปลงพิกัดจากค่าพิกัดฉากยูทีเอ็มไปค่าพิกัดภูมิศาสตร์ โดยใช้ไลบรารี

err = Convert_UTM_To_Geodetic(zn, hemi, x, y, &lat, &lng);

zn คือโซนยูทีเอ็ม hemi คือซีกโลก x และ y คือค่าพิกัดฉากยูทีเอ็ม ส่วนค่าที่คำนวณแล้วจะส่งกลับมาที่ตัวแปร lat, lng ส่วนการแปลงพิกัดอย่างอื่นก็ลองดูได้ตามโค้ด

คอมไพล์และบิวด์ (compile & build)

ทดสอบโดยเมนู “Project” > “Rebuild all” ถ้าเมนูนี้ไม่ขึ้นให้คลิก “Project” > “Reload” ก่อน จากทำการรันโปรแกรมโดย “Run” > “Run” จะเห็นอีมูเลเตอร์ “Display” เลื่อนกดคีย์ ไปที่ไอคอนโปรแกรม จากอีมูเลเตอร์ “Keyboard” แล้วกดคีย์ “EXE”

จะเห็นโปรแกรม “Geographic Cacl” ขึ้นเมนูมา

การใช้งานโปรแกรมนี้พร้อมตัวอย่างไปดูที่ลิ๊งค์นี้ได้

ซอร์สโค้ดหลัก

ท้ายสุดผมเอาซอร์สโค้ดของไฟล์ “main.cpp” มาลงให้ดูเต็มๆ จะเห็นว่าไม่มีอะไรสลับซับซ้อนมาก ง่ายๆครับ

#ifdef __cplusplus
  extern "C" {
#endif

#include "fxlib.h"
#include "string.h"
#include "stdlib.h"
#include "stdio.h"
#include "math.h"
#include "mgrs.h"
#include "tranmerc.h"
#include "utm.h"
#include "utilities.hpp"


#define MAXEDITBUFFER 21
#define NUMDECIMAL 5


void removechar(char* s, const char toremove)
{
  while(s=strchr(s, toremove))   
    memmove(s, s+1, 1+strlen(s+1));
}

int InputMGRSString(int x, int y, unsigned char*prompt, unsigned char*buffer, int bufferSize ){
   unsigned int key, edit_key, return_key = 0;
   int pos, len, len2;
   Cursor_SetFlashMode(1);   // set cursor visibility on
   pos = strlen((char*)buffer);// initially set the cursor to the end of the string

   locate(x, y);
   Print(prompt);
   len2 = strlen((char*)prompt);
   while (!return_key){
      locate(x + len2, y);
      PrintLine(buffer, 22-x);
      locate (x + pos + len2, y);
      GetKey( &key );
      edit_key = 0;
      if ((key >= 0x30 ) && (key <= 0x39)){ edit_key = key; } else if ((key >= 0x41) && (key <= 0x5A)){ edit_key = key; } else{ switch (key){ case KEY_CTRL_DEL : if ( pos > 0 ) pos--;
               len = strlen( (char*)buffer );   // get the current length of the string
               memmove( buffer+pos, buffer+pos+1, len-pos);   // shift the memory: XXYDYYY -> XXXYYY
               break;
            case KEY_CTRL_RIGHT :
               len = strlen( (char*)buffer );   // get the current length of the string
               if ( pos < len ) pos++; break; case KEY_CTRL_LEFT : if ( pos > 0 ) pos--;
               break;
            case KEY_CTRL_UP :
            case KEY_CTRL_DOWN :
               return_key = key;
               break;
            case KEY_CTRL_EXE :
            case KEY_CTRL_EXIT :
               return_key = key;
               break;
            default :
               break;
         };
      }
      if ( edit_key ){
         if ( pos < bufferSize-1 ){ buffer[ pos ] = edit_key; pos++; } } } Cursor_SetFlashMode( 0 ); // set cursor visibility off return ( return_key ); } int AddIn_main(int isAppli, unsigned short OptionNum) { unsigned char buffer[21]; char str[21], s[21]; int editresult; unsigned int key1, key2; double x, y, lat, lng, Lat, Lng; char *slat, *slong, *sangle; long zn; char hemi;//North hemi is 'N', South hemi is 'S' char vBCD[24]; unsigned char sBCD[MAXEDITBUFFER] = ""; int err; unsigned char mgrs[15]; long precision; double *A, *B, *C, *D, *E, *F, *H, *I, *Z; A = (double *)malloc(sizeof(double)); B = (double *)malloc(sizeof(double)); C = (double *)malloc(sizeof(double)); D = (double *)malloc(sizeof(double)); E = (double *)malloc(sizeof(double)); F = (double *)malloc(sizeof(double)); H = (double *)malloc(sizeof(double)); I = (double *)malloc(sizeof(double)); Z = (double *)malloc(sizeof(double)); memset(s, '-', 21); while(1){ Bdisp_AllClr_DDVRAM(); locate(0, 1); Print((unsigned char *)"Geographic Calc"); locate(0, 2); PrintLine((unsigned char*)s, 21); locate(0, 3); PrintLine("[1]:UTM to Geo", 21); locate(0, 4); PrintLine("[2]:Geo to UTM", 21); locate(1, 5); PrintLine("[3]:MGRS to Geo", 21); locate(0, 6); PrintLine("[4]:Geo to MGRS", 21); locate(1, 8); PrintLine("Select 1,2,3 or 4", 21); while (!(key1 >= 0x31) && (key1 <= 0x34)){ GetKey(&key1); } if (key1 == 0x31) { //UTM to Geo Bdisp_AllClr_DDVRAM(); locate(0, 1); Print((unsigned char *)"UTM to Geo"); locate(0, 2); PrintLine((unsigned char*)s, 21); GetAlphaDoubleData('A', A); sprintf((char*)sBCD, (char*) "%.3lf", *A); key2 = EditExpression(0, KEY_CTRL_RIGHT, 3, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "N? : ", 0x04); y = atof((char*)sBCD); SetAlphaDoubleData('A', y); GetAlphaDoubleData('B', B); sprintf((char*)sBCD, (char*) "%.3lf", *B); key2 = EditExpression(0, KEY_CTRL_RIGHT, 4, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "E? : ", 0x04); x = atof((char*)sBCD); SetAlphaDoubleData('B', x); GetAlphaDoubleData('Z', Z); sprintf((char*)sBCD, (char*) "%.3lf", *Z); key2 = EditExpression(0, KEY_CTRL_RIGHT, 5, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "UTM Zone No.? : ", 0x04); if (strchr((char*)sBCD, 0x87) != NULL) { removechar((char*)sBCD, 0x87); hemi = 'S'; } else if(strchr((char*)sBCD, 0x2D) != NULL) { removechar((char*)sBCD, 0x2D); hemi = 'S'; } else hemi = 'N'; zn = atoi((char*)sBCD); SetAlphaDoubleData('Z', zn); //Call function declared in utm.c err = Convert_UTM_To_Geodetic(zn, hemi, x, y, &lat, &lng); if (!err){ Lat = lat * RAD2DEG; Lng = lng * RAD2DEG; slat = degreetodms(fabs(Lat), NUMDECIMAL, 0x2D); if(Lat >= 0)
	        sprintf(str, "Lat= %s N", slat);
	      else
	        sprintf(str, "Lat= %s S", slat);
	      locate(1, 6);
	      Print((unsigned char*)str); 
	      locate(1, 7);
	      slong = degreetodms(fabs(Lng), NUMDECIMAL, 0x2D);
	      if (Lng >= 0)
	        sprintf(str, "Lon= %s E", slong);
	      else
	        sprintf(str, "Lon= %s W", slong);
	      Print((unsigned char*)str);      
	      free(slat);
	      free(slong);
       }
    } else if (key1 == 0x32) { //Geographic to UTM
      Bdisp_AllClr_DDVRAM(); 
      locate(0, 1);
      Print((unsigned char *)"Geo To UTM");
      locate(0, 2);
      PrintLine((unsigned char*)s, 21);

      GetAlphaDoubleData('H', H);
      sangle = degreetodms(*H, NUMDECIMAL, 0x99); 
      if(*H >= 0)
        sprintf(str, "%sN", sangle);
      else
        sprintf(str, "%sS", sangle);
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 3, vBCD, (char*)str, MAXEDITBUFFER - 1, "Lat?: ", 0x04);
      lat = parsedms((char*)str);
      SetAlphaDoubleData('H', lat);
      free(sangle);

      GetAlphaDoubleData('I', I);
      sangle = degreetodms(*I, NUMDECIMAL, 0x99); 
      if(*I >= 0)
        sprintf(str, "%sE", sangle);
      else
        sprintf(str, "%sW", sangle);
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 4, vBCD, (char*)str, MAXEDITBUFFER - 1, "Lon?: ", 0x04);
      lng = parsedms((char*)str);
      SetAlphaDoubleData('I', lng);
      free(sangle);

      Lat = lat * DEG2RAD;
      slat = degreetodms(fabs(Lat), NUMDECIMAL, 0x2D);
      Lng = lng * DEG2RAD;
      slong = degreetodms(fabs(Lng), NUMDECIMAL, 0x2D);
      //Call function declared in utm.c
      err = Convert_Geodetic_To_UTM(Lat, Lng, &zn, &hemi, &x, &y);
      if (!err) {
	      sprintf(str, "North= %11.3lf", y);
	      locate(1, 5);
	      Print((unsigned char*)str);      
	      sprintf(str, "East= %10.3lf", x);
	      locate(1, 6);
	      Print((unsigned char*)str);      
	      sprintf(str, "UTM Zone No= %d%c", zn, hemi);
	      locate(1, 7);
	      Print((unsigned char*)str); 
      }
      free(slat);
      free(slong);      
    } else if (key1 == 0x33) { //MGRS to Geo
      Bdisp_AllClr_DDVRAM(); 
      locate(0, 1);
      Print((unsigned char *)"MGRS to Geo");
      locate(0, 2);
      PrintLine((unsigned char*)s, 21);

      memset(mgrs, 0, 16);
      editresult = InputMGRSString( 1, 3, "MGRS?", buffer, sizeof(buffer) );
      if (editresult) {
        memcpy(mgrs, buffer, 15);
        //Call function that declared in mgrs.c
        err = Convert_MGRS_To_Geodetic((char*)mgrs, &lat, &lng);
        if (!err){
          Lat = lat * 180.0 / PI;
          slat = degreetodms(fabs(Lat), NUMDECIMAL, 0x2D);
          Lng = lng * 180.0 / PI;
          slong = degreetodms(fabs(Lng), NUMDECIMAL, 0x2D);
          if(Lat >= 0)
            sprintf(str, "Lat= %s N", slat);
          else
            sprintf(str, "Lat= %s S", slat);
          locate(1, 4);
          Print((unsigned char*)str);      
          if(Lng >= 0)
            sprintf(str, "Lon= %s E", slong);
          else
            sprintf(str, "Lon= %s W", slong);
          locate(1, 5);
          Print((unsigned char*)str);        
          free(slat);
          free(slong);
        } 
      }
    } else if (key1 == 0x34) { //Geographic to MGRS
      Bdisp_AllClr_DDVRAM(); 
      locate(0, 1);
      Print((unsigned char *)"MGRS to Geo");
      locate(0, 2);
      PrintLine((unsigned char *)s, 21);

      memset(sBCD, 0, MAXEDITBUFFER);
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 3, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "Lat?:", 0x04);
      lat = parsedms((char*)sBCD);
      lat = lat * DEG2RAD;
      memset(sBCD, 0, MAXEDITBUFFER);
      key2 = EditExpression(0, KEY_CTRL_RIGHT, 4, vBCD, (char*)sBCD, MAXEDITBUFFER - 1, "Lon?:", 0x04);
      lng = parsedms((char*)sBCD);
      lng = lng * DEG2RAD;

      //Call function that declared in mgrs.c
      err = Convert_Geodetic_To_MGRS(lat, lng, 5, (char*)mgrs);
      if (!err){
        sprintf(str, "MGRS= %s", mgrs);
        locate(1, 5);
        Print((unsigned char*)str);        
      }     
    }
    GetKey(&key1);
  } //while (1)
  free(A);
  free(B);
  free(C);
  free(D);
  free(E);
  free(F);
  free(H);
  free(I);
  free(Z);
  return 1;
}

#pragma section _BR_Size
unsigned long BR_Size;
#pragma section

#pragma section _TOP

int InitializeSystem(int isAppli, unsigned short OptionNum)
{
    return INIT_ADDIN_APPLICATION(isAppli, OptionNum);
}

#pragma section

#ifdef __cplusplus
}
#endif

การนำโปรแกรมไปติดตั้งบนเครื่องคิดเลข

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

สรุป

การพัฒนาโปรแกรมสำหรับเครื่องคิดเลขไม่มีอะไรยากเย็นมากนัก มีความรู้ภาษาซีขั้นพื้นฐานแบบผมก็ทำได้ เพียงแต่ถ้าต้องการคำนวณอะไรซับซ้อนอาจจะต้องหาไลบรารีที่ท่านอื่นได้พัฒนาเขียนไว้ แต่เครื่องมือพัฒนานี้มีข้อจำกัดอยู่บ้างเช่นคอมไพเลอร์นี้สนับสนุน c standard library ได้ไม่ครบทุกอย่าง ตัวอย่างเช่นฟังก์ชั่น strtok() ที่ตัดสตริงออกตามตัวคั่นก็ไม่สนับสนุน ดังนั้นการเลือกไลบรารีที่คนอื่นได้ทำไว้ก็ต้องพิจารณาส่วนนี้ด้วย ส่วนการรับข้อมูลจากผู้ใช้ สำหรับผมแล้วที่มีอยู่ตอนนี้เกือบจะเพียงพอ เพราะงานสำรวจก็จะมีแค่ป้อนมุม ระยะทาง เป็นหลัก ติดตามกันต่อไปครับ

ติดปีกเครื่องคิดเลขเทพ Casio fx 9860G II SD ด้วยโปรแกรมภาษาซีบน AddIn ตอนที่ 1 โปรแกรมแปลงพิกัดภูมิศาสตร์ (Geographic Calc)

ติดปีกเครื่องคิดเลขเทพ Casio fx 9860G II SD ด้วยโปรแกรมภาษาซีบน AddIn ตอนที่ 1 โปรแกรมแปลงพิกัดภูมิศาสตร์ (Geographic Calc)

รอคอยมานานแต่ไม่รู้ว่าสิ่งที่รอคอยมันคืออะไร

สำหรับคนที่เคยเขียนโปรแกรมลงเครื่องคิดเลขคาสิโอ ถ้าเคยเขียนโปรแกรมมิ่งบนระบบใหญ่ๆมาก่อนเช่นจาวา ซี หรือไพทอน จะรู้สึกว่าโดนมัดมือมัดเท้าทำอะไรไม่ถนัด ภาษาเบสิคของคาสิโอ (basic casio) ก็ดูจะหน่อมแน๊ม ตัวแปรก็จำกัดไม่กี่ตัว เมมโมรีสำหรับเก็บโปรแกรมก็น้อยนิดเดียว เขียนฟังก์ชั่นก็ไม่ถนัด ก็เลยได้แต่โปรแกรมอะไรที่ง่ายๆ ใช้ตัวแปรไม่มาก  แต่ไม่นานที่ผ่านมา เผอิญไปค้นหาในอินเทอร์เน็ต โดยที่หาโปรแกรมแบบ basic casio บนเครื่องคิดเลขระดับเทพในวงการสำรวจบ้านเราคือ fx-9860G II SD ที่ผมเคยร่ำๆจะซื้อหามาใช้หลายเที่ยวแต่ติดที่ความรู้สึกว่าแพงไปนิดเมื่อเทียบกับ fx-5800P ที่ใช้อยู่ โปรแกรมที่ค้นหาก็ไม่ได้มีอะไรมากแค่เอามาเปรียบเทียบอัลกอริทึ่มที่ผมมีอยู่ บังเอิญไปเจอว่าการเขียนโปรแกรม AddIn ต้องใช้ SDK (Software Development Kit) ที่ต้องใช้ภาษาซี ก็เลยสะดุดตา ลองค้นเข้าไปอีกหน่อย ก็พออนุมานได้ว่าสามารถเขียนโปรแกรมอะไรก็ได้แบบ AddIn ให้กับเครื่องคิดเลข ที่ไม่ติดจำกัดด้านโครงสร้างภาษาเพราะใช้ภาษาซี ที่คาสิโอเตรียมคอมไพเลอร์ ไลบรารีเครื่องมือพัฒนาโปรแกรมด้านภาษาซีมาพอประมาณ สุดท้ายผมก็เลยมานึกว่า ก่อนหน้านี้ผมคงต้องรอคอยอะไรบางอย่างมานานแต่ไม่รู้ว่าคืออะไร จนกระทั่งได้เจอสิ่งนี้ 🙂 มันใช่เลย ถึงแม้ตอนเจอดูเหมือนผมจะมาสายไปบ้างก็ตาม

อารมณ์มัน Back to school คือความสนุกสนานได้กลับมาอีกครั้ง ผมเคยพูดถึงว่าเครื่องรุ่นเทพสมัยแต่ก่อนคือ Casio fx-880P ที่เขียนภาษาเบสิค(แบบกำกับด้วยหมายเลขบรรทัด) เวลาพกเครื่องคิดเลขรุ่นนี้ ถ้าเอาเท่ห์ก็เอาเหน็บที่กระเป๋าหลังของกางเกงยีนส์ แต่บ่อยครั้งที่ลืมนั่งทับจนเครื่องพัง ที่สามารถเขึยนโปรแกรมได้พอประมาณ แต่ปัญหาคือเมมโมรีที่จัดเก็บโปรแกรมมาน้อย ถึงแม้สามารถซื้อแรมขนาด 32KB มาเพิ่มได้ก็ตาม เคยเขียนโปรแกรม Traverse เล่นๆลงไปเขมือบเมมโมรีไปเกินครึ่ง จนต้องลบโปรแกรมอื่นทิ้งไป ถึงจะใส่ได้ การจะโอนโปรแกรมไปหาเครื่องอื่นก็แสนยากเย็นกระไร เพราะต้องหาสายลิ๊งค์ สมัยก่อนไม่มีอีเบย์ ก็เลยใครอยากได้โปรแกรมอะไรก็ต้องพิมพ์เองสดๆลงไปในเครื่อง ประมวลผลดูผิดตรงไหนก็ตามไปแก้ สำหรับเครื่องคิดเลขในทศวรรษนี้ไม่ต้องทำแบบนั้นแล้วครับมีสายลิ๊งค์มาให้ หรือรุ่น fx-9860G II SD ก็มี SD card มาให้สามารถโอนโปรแกรมให้กันได้สะดวก

รู้จัก Casio fx-9860G II SD

เครื่องรุ่นนี้ออกเก็บเกี่ยวความสำเร็จตามหลัง fx-9750G โดยที่ผลิตออกมาสองรุ่น รุ่นแรกเคสสีเงินส่วนคีย์บอร์ดสีน้ำเงิน ใช้ CPU SH3 รุ่นที่สองเป็นรุ่นล่าสุดเคสสีน้ำเงินเข้มส่วนคีย์บอร์ดสีขาวใช้ CPU SH4a มีเมมโมรีใช้งาน 62 KB (ขนาดน่าสงสารมาก) มีพื้นที่จัดเก็บโปรแกรม (storage memory) เป็น 1.5 MB ที่ผมประเมินดูโปรแกรมขนาดกลางๆสำหรับเครื่องคิดเลขขนาด น่าจะประมาณ 50000 Bytes ถ้าพื้นที่จัดเก็บโปรแกรม 1.5 MB น่าจะใส่โปรแกรมได้ไม่ต่ำกว่า 30 โปรแกรมเลยทีเดียว โดยรวมการประมวลผลเร็วครับ ตามความเข้าใจผมตัว OS ของรุ่นนี้น่าจะกินเมมโมรีไม่มากนัก ที่ผมชอบอีกอย่างคือพื้นที่การแสดงผล ถ้าเอาแบบแสดงตัวอักษรอย่างเดียว ได้ทั้งหมด 8 แถว (row) และแถวละ 21 ตัวอักษร ถามว่าพอไหม ก็ตอบได้ว่าพอครับแบบเบียดเสียดไปหน่อย แต่ยังโอเคกว่ารุ่น fx-5800P ที่มีแค่ 2 บรรทัด แต่อย่างไรก็ตามยังมีโหมดกราฟฟิคมีความละเอียดกว้าง x สูง = 127 x 63 สำหรับวาดกราฟ ก็มาดูขนาดโปรแกรมแปลงพิกัดภูมิศาสตร์ของผม UTMGeo.g1a คือโปรแกรมที่คอมไพล์และบิวท์ (compile & build) มาแล้ว ขนาดประมาณตามรูป 78760 ไบต์ ส่วนโปรแกรมสองโปรแกรมด้านบน (ARCCENPT.g1a และ INTERSCT.g1a) ผมก็เขียนเหมือนกันแต่ขนาดเล็กกว่า

ตามล่าเครื่องมือพัฒนาโปรแกรม SDK (Software Development Kit)

เมื่อรู้ว่าใช้ภาษาซีเขียนได้ ผมก็ตามหาเครื่องมือเพื่อพัฒนาโปรแกรม แต่เนื่องจากรุ่นนี้ออกมาได้หลายปีร่วมๆสิบปีแล้ว (ออกมาปี 2009) เข้าไปในเว็บไซต์ของคาสิโอแต่กลับพบกับผิดหวัง ไม่มีลิ๊งค์ให้ดาวน์โหลด (Link ขาดไปนาน) ทั้งๆที่คู่มือต่างๆเช่นการใช้งาน SDK, ไลบรารี ต่างๆก็ยังมีให้ดาวน์โหลดปกติแต่เครื่องมือพัฒนาโปรแกรมตั้งแต่เขียนโปรแกรม คอมไพล์ บิวท์ กลับหายไป สุดท้ายต้องลงใต้ดินตามหา จนเจอยังมีคนปล่อยให้ดาวน์โหลดได้ แต่ต้องใช้ระยะเวลาความพยายามเป็นอาทิตย์เหมือนกัน ผมจะไม่แสดงลิ๊งค์นี้เพราะอาจติดขัดกับลิขสิทธิ์ของคาสิโอได้ (ถ้าใครอยากได้ก็ขอมาหลังไมค์ละกันครับ) เมื่อได้มาแล้วก็มาลงบนคอมพิวเตอร์โน๊ตบุ๊ค วินโดส์ 10 และจอของผมเป็น 4K ก็ไม่ได้มีปัญหาอะไร สามารถเปิดโปรแกรมมาได้ปกติ เครื่องมือพัฒนาโปรแกรมเรียกว่า integrated development environment (IDE) ตั้งแต่ปล่อยมาปี 2007 Casio ไม่เคยอัพเดทอีกเลย เครื่องมือนี้ใช้คอมไพเลอร์ของ Renesas SHC ซึ่งอิงภาษาซีของ ANSI C standard (C89)

เริ่มแรกใช้งานกับปัญหาที่ประสบ

แต่พอเริ่มคอมไพล์โปรแกรมทดสอบเล็กๆดูกลับมีปัญหาเล็กๆน้อยๆ เช่น ** L2011 (E) Invalid parameter specified in option “input” : “”C:\Program Files (x86)\CASIO\fx-9860G SDK\OS\FX\lib\setup.obj”” วิธีการแก้ไข ให้ถอนโปรแกรมไปติดตั้งใหม่ แต่ตอนติดตั้งให้ติดเลือกติดตั้งที่รากของไดรว์ C: (ไม่เลือกดีฟอลต์คือติดตั้งลง C:\Program fils(x86) เพราะโปรแกรมนี้รุ่นเก่าไม่ชอบ path ทีมีตัว space) ปัญหาเล็กๆน้อยๆ เหล่านี้พอหาได้ตามฟอรั่มที่เกี่ยวข้องกับเครื่องคิดเลขของคาสิโอครับ แต่แล้วเส้นทางนี้ไม่ได้โรยด้วยกลีบกุหลาบ ปัญหาที่นึกไม่ถึงคือ user interface ทางคาสิโอไม่ได้เตรียม document ไว้ให้เลย พวกสิ่งเหล่านี้ได้แก่การ input แม้กระทั่งการอ่านข้อมูลจากตัวแปรตัวอักษร A-Z ก็ไม่ได้ทำไว้ ข้อมูลเป็นตัวเลข เป็นสตริง ผมอาศัยไปอ่านตามฟอรั่มที่มีหลายคน hack ไว้ ตรงนี้เสียเวลาไปหลายสิบวันกว่าจะแกะและจูนได้

โปรแกรมแรกเป็นกรณีศึกษา -เขียนโปรแกรมแปลงพิกัดภูมิศาสตร์ (Geographic Calc)

จั่วหัวไปเหมือนโปรแกรมจะใหญ่โต แต่เปล่าเลยผมเคยเขียนโปรแกรมแปลงพิกัดระหว่าง UTM และค่าพิกัดภูมิศาสตร์ (Geographic) บน fx 5800P ก็ไม่ได้ยากเย็นอะไรมากเพราะการแปลงพิกัดเหลานี้มีสูตรที่แน่นอนถึงแม้สูตรจะยาวไปหน่อยก็ตาม แต่ก็ไม่ได้ยากเย็นอะไร ผมมีเรื่องการแปลงพิกัดค้างคาอยู่นิดหนึ่งคือในโปรแกรม Surveyor Pocket Tools ในส่วนการแปลงพิกัดจะสังเกตเห็นว่าไม่มีระบบพิกัด MGRS (Military Grid Reference System) ซึ่งสำหรับพลเรือนอย่างพวกเรา คงไม่มีโอกาสได้ใช้งานเท่าไหร่นัก ผมค้นดูไลบรารีที่สามารถแปลงพิกัดได้ตาม github ไปพบมาอันหนึ่งชื่อ mrgs พัฒนาโดย Howard Butler ซึ่งไลบรารีที่เขียนไว้ไม่ใหญ่มาก นอกจากแปลงพิกัด Transverse Mercator ได้ยังแปลง MGRS ได้ และโปรแกรมในรุ่นนี้ผมขอจำกัดแค่ดาตั้ม “WGS84

ส่วนผมเองต้องบอกก่อนว่าไม่ใช่แฟนพันธุ์แท้ภาษาซี พอจะเขียนได้แต่ไม่ได้เก่งกาจนัก ดังนั้นโปรแกรมที่เขียนขึ้นมาอาจจะมีส่วนใดส่วนหนึ่งที่เยิ่นเย้อไปบ้าง

ลอง Military Grid Reference System (MGRS) ดูสักตั้ง

ในส่วนระบบพิกัด MGRS ซึ่งผมเห็นว่ามันแปลกดีที่ระบบนี้เอาตัวอักษรและตัวเลขแบ่งเป็นกริดมาขมวดรวมกันก็กลายเป็นค่า coordinate ได้ ลองดูรูปแบบดังตัวอย่างด้านล่าง

    • 46Q …………………GZD only, precision level 6° × 8° (in most cases)
    • 46QFJ ……………….GZD and 100 km Grid Square ID, precision level 100 km
    • 46QFJ 1 6 ……………precision level 10 km
    • 46QFJ 12 67 ………….precision level 1 km
    • 46QFJ 123 678 ………..precision level 100 m
    • 46QFJ 1234 6789 ………precision level 10 m
    • 46QFJ 12345 67890 …….precision level 1 m

ในเบื้องต้นผมขอใช้ไลบรารีนี้เพื่อเป็นกรณีศึกษา เพื่อลองเขียนโปรแกรมแปลงพิกัด MGRS ดูบ้าง ซึ่งระบบพิกัดนี้เครื่องคิดเลข fx-5800P ทำไม่ได้แน่นอนครับเพราะเครื่องคิดเลขไม่มีระบบรับข้อมูลเป็นสตริง (ยาวสุดประมาณ 15 ตัวอักษร) นอกจากไม่มีระบบรับข้อมูลสตริงแล้ว ไม่มีไลบรารีตัดสตริงออกมาเป็นท่อนๆ

เส้นทางและระยะเวลาในการพัฒนา

เนื่องจากโปรแกรมแปลงพิกัดนี้เป็นโปรแกรมเล็กๆ ไลบรารีที่ผมไปเอามาใช้จาก github ก็ใช้ง่ายสะดวก แต่ติดปัญหาที่ผมบอกไปแล้วคือระบบติดต่อผู้ใช้รับข้อมูลตัวเลข ตัวอักษรทาง casio ไม่ได้เปิดเผยเอกสาร บางอย่างต้องเขียนเองเช่นการรับข้อมูลเป็นสายสตริงเช่นค่าพิกัด MGRS (ตัวอย่างเช่น “18SVK8588822509”) บางอย่างไปหาตามฟอรั่ม เลยใช้เวลาสำหรับโปรแกรมแรกนี้ประมาณหนึ่งอาทิตย์กว่าๆ ในตอนนี้โปรแกรมเล็กๆนี้ก็เสร็จพอใช้งานได้แล้ว คุณสมบัติของโปรแกรมนี้คือค่าพิกัด latitude/longitude ที่แปลงมาจาก MGRS หรือ UTM สามารถแสดงผลได้ในทศนิยมที่ห้า ซึ่งจะเทียบเท่ากับหน่วยมิลลิเมตร ที่เราชาวสำรวจที่ต้องใช้ ถ้าใครเคยใช้โปรแกรมแปลงพิกัด UTM <==> Geo ที่ผมเขียนด้วย fx-5800P จะสังเกตเห็นว่าคำนวณแล้วได้ทศนิยมแค่สองตำแหน่งเท่านั้น ข้อได้เปรียบของ fx-9860G II คือสถาปัตยกรรมของเครื่องรุ่นนี้สามารถใช้ตัวแปร double ได้ ซึ่งในงานสำรวจนั้นเพียงพออยู่แล้ว

มาดาวน์โหลดโปรแกรมไปทดสอบ

เมื่อผม compile และ build โปรแกรมในเครื่องมือพัฒนาโปรแกรมของคาสิโอ แล้วจะได้ไฟล์ที่นามสกุล G1A (ตัว A คงหมายความว่า AddIn) ถ้าสนใจก็ไปดาวน์โหลดได้ที่หน้าดาวน์โหลด เมื่อได้ไฟล์มาแล้วชื่อ “UTMGeo.G1A” จากนั้นให้ดึง SD card ที่เสียบอยู่ด้านบนเครื่องคิดเลข fx-9860G II SD แล้วนำมาเสียบที่เครื่องพีซีหรือโน๊ตบุ๊ค เมื่อเปิดด้วย windows explorer จากนั้นสร้างโฟลเดอร์ใหม่ อย่างของผมตั้งชื่อ “Survey Addin Programs” แล้วก็อปปี้ไฟล์ “UTMGeo.G1A” ไปไว้ที่โฟลเดอร์ดังกล่าวนี้

จากนั้นดึง SD card เอาไปเสียบที่เครื่องคิดเลขเหมือนเดิม จากนั้นกดปุ่ม “MENU” ที่คีย์บอร์ดของเครื่องคิดเลข จะเห็นไอคอนของโปรแกรม AddIn ขึ้นมาทั้งหมด ใช้คีย์บอร์ดลูกศรเลื่อนไปที่ “MEMORY” กด “EXE”

  • จะเห็นตัวหนังสือ Memory Manager พร้อมเมนูให้เลือก เลือกกดคีย์บอร์ด “F3” เพื่อเลือก F3:SD Card
  • จะเห็นโฟลเดอร์ ที่อยู่ในเครื่องคิดเลข จะเป็นชื่อสั้น 8.3 แบบระบบปฏิบัติการ DOS สมัยแต่ก่อน เลื่อนไปที่ [SURVEY~2] กด “EXE”
  • จะเห็นชื่อไฟล์ “UTMGeo.G1A” ที่เราก็อปปี้มาจากโน๊ตบุ๊คคอมพิวเตอร์นั่นเอง กด “F1” (SEL) และกดปุ่ม “F2” (COPY)
  • ที่นี้จะมีไดอะล็อกเล็กๆให้เลือกปลายทาง กดคีย์เลข 2 เลือกโฟลเดอร์ปลายทางเป็น “ROOT” กด “EXE” ถ้ามีไฟล์อยู่แล้วให้ยืนยันการเขียนทับ “Yes” ด้วยกดคีย์ “F1”
  • จากนั้นก็กดคีย์ “EXIT” หลายๆครั้ง สุดท้ายกดคีย์ “MENU” กดลูกศรเลื่อนลงไปด้านล่าง จะเห็นไอคอน “UTM Geo” จากนั้นกดปุ่ม “EXE”

ทดสอบการใช้งานโปรแกรมแปลงพิกัด Geographic Calc

เมื่อกด “EXE” เข้าไปแล้วจะเห็นบรรทัดบนสุดแสดงชื่อโปรแกรม “Geographic Calc” มีเมนูแบบง่ายๆ 4 เมนูให้เลือกคือ ต้องการเลือกตัวไหนก็กดตัวเลขตามเมนู

    1.  UTM to Geo – แปลงพิกัดจากระบบพิกัดฉากยูทีเอ็มไปยังค่าพิกัดภูมิศาสตร์ Latitude/Longitude
    2. Geo to UTM – แปลงพิกัดจากระบบพิกัดภูมิศาสตร์ไปยังระบบพิกัดฉากยูทีเอ็ม
    3. MGRS to Geo – แปลงพิกัดจากระบบพิกัด MGRS ไปยังระบบพิกัดภูมิศาสตร์
    4. Geo to MGRS – แปลงพิกัดจากระบบพิกัดภูมิศาสตร์ไปยังระบบพิกัด MGRS

แปลงพิกัดจากระบบพิกัดฉากยูทีเอ็มไปยังค่าพิกัดภูมิศาสตร์ (UTM to Geo)

ที่เมนูกดคีย์เลข “1” เข้าไปโปรแกรมจะถามค่าพิกัด North, East และตัวเลขของโซนยูทีเอ็ม ลองป้อนข้อมูลตามตัวอย่าง

จะได้ผลลัพธ์ดังต่อไปนี้ ครับตามที่กล่าวไปแล้วได้ทศนิยมค่าแลตติจูด ลองจิจูด ตำแหน่งที่ห้า

แปลงพิกัดจากพิกัดภูมิศาสตร์ไปยังระบบพิกัดฉากยูทีเอ็ม (Geo to UTM)

ที่เมนูกดเลข “2” โปรแกรมจะถามค่าพิกัดภูมิศาสตร์ สามารถป้อนทศนิยมได้มากกว่า 5 ตัว ป้อนค่ามุมตัวคั่นองศา ลิปดา ฟิลิปดาให้ใช้เครื่องหมายลบ (-) ค่าแลตติจูดลงท้ายให้ป้อนตัวอักษร ถ้าซึกโลกเหนือให้ป้อน “N” ตามหลัง ตามเป็นซีกโลกใต้ให้ป้อน “S” หรือค่าลองจิจูดซึกโลกตะวันตกให้ป้อนคำว่า “W” ลงท้าย ซีกโลกตะวันออกให้ป้อน “E” ลงท้าย ดูตัวอย่าง

เมื่อกด “EXE” จะได้ผลลัพธ์ดังนี้

แปลงค่าพิกัดจาก MGRS ไปยังค่าพิกัดภูมิศาสตร์ (MGRS to Geo)

ที่เมนูกดปุ่ม “3” เลือก โปรแกรมจะถามค่าพิกัด MGRS ป้อนไปดังรูปด้านล่าง

กด “EXE” จะได้ผลลัพธ์ค่าพิกัดภูมิศาสตร์

แปลงค่าพิกัดจากค่าพิกัดภูมิศาสตร์ ไปยัง MGRS  (Geo to MGRS)

ที่เมนูกดเลข “4” ลองป้อนค่าพิกัดแลตติจูด ลองจิจูดเข้าไปดังรูป

กด “EXE” จะได้ค่าผลลัพธ์ดังรูป

สรุป

ในขณะที่ลองเขียนโปรแกรมสำหรับเครื่องคิดเลข fx-9860G II นี้ ผมยังทำงานที่บังคลาเทศ ใช้เครื่องคิดเลขของน้องๆ แต่ด้วยความประทับใจเครื่องรุ่นนี้เลยสั่งซื้อเครื่องที่เมืองไทยส่งไปที่บ้านรอกลับไปค่อยไปลองเครื่องใหม่อีกที (สั่งจาก mr.finance ที่รับของแล้วค่อยโอนเงินอีกที บริการประทับใจครับ) ด้วยสนนราคาประมาณตอนนี้สี่พันบาทปลายๆ รวม SD card  มีความรู้สึกว่าคุ้มค่า ไม่ลังเลเหมือนก่อน ผมมีโครงการจะเขียนโปรแกรมเล็กๆด้วยภาษาซีอีกหลายโปรแกรมเพื่อแจกจ่ายเป็นโปรแกรมให้พี่ๆน้องๆในวงการสำรวจได้ใช้งานกันโดยไม่ได้คิดมูลค่า

ขอเพิ่มเติมอีกนิดครับ บทความที่นำเสนอมานี้ไม่ได้มีเจตนาส่งเสริมการขายเครื่องคิดเลขรุ่น fx-9860G II SD นี้ให้ขายดีขึ้นแต่อย่างใด สำหรับน้องๆนักศึกษาหรือช่างสำรวจที่จบมาทำงานใหม่ๆ เครื่องคิดเลข fx-5800P สามารถใช้งานได้ทั่วๆไปได้เพียงพอ โปรแกรมที่มีขายแและแจกจ่ายในวงการบ้านเราก็สามารถหามาใช้งานกันได้อย่างเหลือเฟือ และราคาเครื่องคิดเลข fx-5800P ก็พอจะซื้อหามาใช้งานได้ แต่สำหรับเครื่องคิดเลขรุ่น fx-9860G II SD นั้นราคามากกว่า fx-5800P ประมาณ 2-3 เท่า ถ้ามีเงินเหลือใช้ก็หาซื้อหามาใช้กันได้ สำหรับคนที่มีงานทำแล้วก็พอจะสามารถเก็บเงินซื้อได้

ผมเขียนบทความนี้เพื่อวงการศึกษาช่างสำรวจบ้านเราที่สนใจเรื่องโปรแกรมมิ่งสามารถจะพัฒนาโปรแกรมภาษาซีบนเครื่องคิดเลขรุ่นนี้ได้ โดยที่มีไม่มีข้อจำกัดด้านภาษาโปรแกรมมิ่งแต่อย่างใด เหมือนภาษา basic casio อาจจะส่งผลให้ในอนาคต มีโปรแกรมที่พัฒนาโดยบุคคลากรท่านอื่นๆ เข้ามาสู่วงการนี้มากขึ้น และได้ตัวโปรแกรมงานสำรวจก็มีความหลากหลายและความสามารถมากขึ้น ในบทความตอนหน้าไม่กี่ตอนจากนี้ไปจะมีบทความ แนะนำการใช้เครื่องมือพัฒนาโปรแกรม SDK (Software Development Kit) ของ casio และเทคนิคการใช้เครื่องมืออื่นๆที่ แฮกเกอร์เครื่องคิดเลขรุ่นนี้ได้ reverse-engineering เขียนเผยแพร่ไว้ หรือแม้กระทั่งใช้ฟังก์ชั่นที่ไม่ได้เปิดเผยจากทาง casio เองก็ตาม พบกันใหม่ครับ

การเขียนโปรแกรมคำนวณการแปลงค่าพิกัดระหว่าง UTM Grid และ Geographic (Lat/Long) ด้วย Lazarus และ Delphi (ตอนที่ 3)

  • จากตอนที่ 2 จะเห็นโค๊ดที่ผม post 2 unit คือ GeoEllipsoids.pas และ GeoCompute.pas ถ้าสนใจก็ copy ไปวางที่ Text Editor แล้ว Save ชื่อไฟล์ตามที่ผม เราจะเริ่มต้นสร้าง New Project บน Lazarus จากนั้น Add file “GeoEllipsoids.pas” และ “GeoCompute.pas” เข้ามาในโปรเจคใหม่ที่สร้างขึ้น
  • ผมทิ้งท้ายเรื่องสร้างโปรเจคใหม่ในตอนที่ 1 ก่อนจะกล่าวถึง 2 unit ในตอนที่ 2 กลับมาที่ Lazarus อีกครั้ง
Lazarus 0.9.29 (beta) บน windows
Lazarus 0.9.29 (beta) บน windows
  • จะเห็นฟอร์ม unit1 นั้นเปล่าๆ ยังไม่ทีอะไร และ Project Inspector ด้านขวามือก็ยังว่างขึ้นเพียงแต่ project1.lpr และ unit1.pas เ่ท่านั้น ตรง component palette ยังมี component อีกมากมายของ Lazarus ที่ยังไม่ได้ติดตั้งเข้ามา ซึ่งจะอยู่ในไดเรคทอรี components ของ Lazarus สนใจตัวไหนก็ ใช้เมนูหลัก Packages > Open package file (.lpk) … ทำการ compile และ install ในตอนนี้ยังไม่ได้ใช้ตัวไหนเป็นพิเศษ จะใช้ standardd เท่านั้น
  • เมื่อสร้างโปรเจคใหม่แล้ว จุดที่สำคัญที่ต้องตั้งเลยก็คือ OS และ CPU Platform ที่เมนูหลักเลือก Project > Compiler Options… จะเห็น dialog ให้ตั้งค่าดังรูปด้านล่าง
ตั้งค่า OS และ Platform
ตั้งค่า OS และ Platform
  • ที่ Project Inspector ให้คลิกที่เครื่องหมาย + เพื่อ Add file…
Add file to project
Add file to project
  • Add file ที่ผม post ไว้ตอนที่ 2 เข้ามา 2 ไฟล์ดังรูปด้านล่าง
Project Inspector หลังจากเพิ่มไฟล์
Project Inspector หลังจากเพิ่มไฟล์
  • ที่ unit1 ทำการเพิ่ม object จาก standard palette เข้าไปตั้ง caption และชื่อดังรูปด้านล่าง
Create object on form.
Create object on form.
  • โค๊ดข้างล่างเป็นของ unit1 เขียน event เชื่อมต่อกับ ojbect บนฟอร์มแล้ว
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, FileUtil, LResources, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, GeoEllipsoids, GeoCompute;

type

{ TForm1 }

TForm1 = class(TForm)
cmdEx1: TButton;
cmdEx2: TButton;
cmdCompute: TButton;
cmdClose: TButton;
cmbEllipsoids: TComboBox;
cmbHem: TComboBox;
Label7: TLabel;
txtZoneNo: TEdit;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
txtE: TEdit;
txtLat: TEdit;
Label1: TLabel;
Label2: TLabel;
radComputeType: TRadioGroup;
txtLong: TEdit;
txtN: TEdit;
procedure cmdCloseClick(Sender: TObject);
procedure cmdComputeClick(Sender: TObject);
procedure cmdEx1Click(Sender: TObject);
procedure cmdEx2Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure radComputeTypeClick(Sender: TObject);
private
{ private declarations }
FEllipses : TEllipsoidList;
public
{ public declarations }
end;

var
Form1: TForm1;
function Coordinate(const X, Y : double) : TCoordinate;
implementation

function Coordinate(const X, Y : double) : TCoordinate;
begin
result.X := X;
result.Y := Y;
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
i : integer;
begin
FEllipses := TEllipsoidList.Create;
for i := 0 to FEllipses.Count - 1 do
begin
cmbEllipsoids.Items.Add(TEllipsoid(FEllipses.Objects[i]).EllipsoidName);
end;
cmbEllipsoids.ItemIndex := FEllipses.Count - 1; //Default with WGS84
//start with UTM to Geographic computation.
txtN.Enabled := true;
txtE.Enabled := true;
txtZoneNo.Enabled := true;
cmbHem.Enabled := true;

txtLat.Readonly := true;
txtLong.Readonly := true;
end;

procedure TForm1.cmdCloseClick(Sender: TObject);
begin
Close;
end;

procedure TForm1.cmdComputeClick(Sender: TObject);
var
utm2geo : TUTM2Geo;
geo2utm : TGeo2UTM;
ell : TEllipsoid;
x, y : double;
utmproj : TUTMProjection;
geocoor, utmcoor : TCoordinate;
begin
ell := FEllipses.FindEx(cmbEllipsoids.Text);

if (radComputeType.ItemIndex = 0) then //UTM to Geographic
begin
x := strtofloat(txtE.Text);
y := strtofloat(txtN.Text);
utmcoor := Coordinate(x, y);
utmproj.ZoneNo := strtoint(txtZoneNo.Text);
if (cmbHem.ItemIndex = 0)then
utmproj.LatHem := hemNorth
else
utmproj.LatHem := hemSouth;

try
utm2geo := TUTM2Geo.Create;
utm2geo.Ellipsoid := ell;
utm2geo.UTMProjection := utmproj;
utm2Geo.UTMCoordinate := utmcoor;
utm2Geo.Compute;
txtLat.Text := format('%11.8f', [utm2geo.GeoCoordinate.Y]);
txtLong.Text := format('%11.8f', [utm2geo.GeoCoordinate.X]);
finally
utm2geo.Free;
end;
end
else  //Geographic to UTM
begin
x := strtofloat(txtLong.Text);
y := strtofloat(txtLat.Text);
geocoor := Coordinate(x, y);

try
geo2utm := TGeo2UTM.Create;
geo2utm.Ellipsoid := ell;
geo2utm.GeoCoordinate := geocoor;
geo2utm.Compute;
utmproj := geo2utm.UTMProjection;
txtN.Text := format('%11.3f', [geo2utm.UTMCoordinate.Y]);
txtE.Text := format('%11.3f', [geo2utm.UTMCoordinate.X]);
txtZoneNo.Text := format('%3d', [utmproj.ZoneNo]);
if (utmproj.LatHem = hemNorth) then
cmbHem.ItemIndex := 0
else
cmbHem.ItemIndex := 1;
finally
geo2utm.Free;
end;
end
end;

procedure TForm1.cmdEx1Click(Sender: TObject);
begin
radComputeType.ItemIndex := 0; //UTM to Geographic
txtN.Text := '3885802.712';
txtE.Text := '468327.864';
txtZoneNo.Text := '11'; //UTM Zone = 11
cmbHem.ItemIndex := 0; //North

txtLat.Text := '';
txtLong.Text := '';
end;

procedure TForm1.cmdEx2Click(Sender: TObject);
begin
radComputeType.ItemIndex := 1; //Geographic to UTM
txtLat.Text := '-37.6528211388889';
txtLong.Text := '143.926495527778';

txtN.Text := '';
txtE.Text := '';
txtZoneNo.Text := '';
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
FEllipses.Free;
end;

procedure TForm1.radComputeTypeClick(Sender: TObject);
begin
if (radComputeType.ItemIndex = 0) then // UTM to Geographic
begin
cmdCompute.Caption := '&amp;lt;==';
txtN.Enabled := true;
txtE.Enabled := true;
txtZoneNo.Enabled := true;
cmbHem.Enabled := true;
txtN.SetFocus;
txtLat.Readonly := true;
txtLong.Readonly := true;
end
else
begin
cmdCompute.Caption := '==&amp;gt;';   //Geographic to UTM
txtN.Readonly := true;
txtE.Readonly := true;
txtZoneNo.Readonly := true;
cmbHem.Readonly := true;
txtLat.Enabled := true;
txtLong.Enabled := true;
txtLat.SetFocus;
end;
end;

initialization
{$I unit1.lrs}

end.
  • ดูที่ procedure TForm1.FormCreate(Sender: TObject); เป็น event ของ form1 ผมดึง Ellipsoids (FEllipsoids) ที่สร้างไว้เป็น object มา iterate ด้วย for loop ลงไปที่ combo box ชื่อ cmbEllipsoids เวลาผู้ใช้คลิกเลือก Ellipsoid จาก combo box จะได้เพียงแต่ text ออกมาเราจะใช้ text ค้นกลับไปที่ FEllipses( TEllipsoidList) เพื่อดึง object ที่แสดงรูปทรงของ Ellipsoid
  • เมื่อผู้ใช้เลือกคลิกที่ radio group “Computation type” จะเลือกว่าจะคำนวณจาก UTM ไป Geographic หรือในทางกลับกัน อีเวนท์ของ radio group นี้อยู่ที่ procedure TForm1.radComputeTypeClick(Sender: TObject); จะได้ใช้ if clause เลือกการคำนวณถูกว่าจะใช้ class ไหน ระหว่าง TUTM2Geo หรือ TGeo2UTM
  • เพื่อให้ดูง่ายยิ่งขึ้นผมเพิ่มตัวอย่างเพื่อจะใช้คำนวณดู โดยกำหนดอยู่ที่ button ชื่อ cmdEx1 และ cmdEx2 พร้อมทั้งเขียนอีเวนท์ให้ 2 procedure คือ procedure TForm1.cmdEx1Click(Sender: TObject); และ procedure TForm1.cmdEx2Click(Sender: TObject); ตามลำดับ
procedure TForm1.cmdEx1Click(Sender: TObject);
begin
radComputeType.ItemIndex := 0; //UTM to Geographic
txtN.Text := '3885802.712';
txtE.Text := '468327.864';
txtZoneNo.Text := '11'; //UTM Zone = 11
cmbHem.ItemIndex := 0; //North

txtLat.Text := '';
txtLong.Text := '';
end;

procedure TForm1.cmdEx2Click(Sender: TObject);
begin
radComputeType.ItemIndex := 1; //Geographic to UTM
txtLat.Text := '-37.6528211388889';
txtLong.Text := '143.926495527778';

txtN.Text := '';
txtE.Text := '';
txtZoneNo.Text := '';
end;
  • สุดท้าย button ที่เป็นรูปลูกศรชี้ไปซ้ายเมื่อเลือกคำนวณจาก UTM ไป Geographic และชี้ไปด้านขวาเมื่อเลือก Geographic ไป UTM เมื่อป้อนค่าพิกัดที่เหมาะสมแล้วทำการคลิกที่ button ก็จะไปปลุกอีเวนท์ ดังโค๊ดด้านล่าง
procedure TForm1.cmdComputeClick(Sender: TObject);
var
utm2geo : TUTM2Geo;
geo2utm : TGeo2UTM;
ell : TEllipsoid;
x, y : double;
utmproj : TUTMProjection;
geocoor, utmcoor : TCoordinate;
begin
ell := FEllipses.FindEx(cmbEllipsoids.Text);

if (radComputeType.ItemIndex = 0) then //UTM to Geographic
begin
x := strtofloat(txtE.Text);
y := strtofloat(txtN.Text);
utmcoor := Coordinate(x, y);
utmproj.ZoneNo := strtoint(txtZoneNo.Text);
if (cmbHem.ItemIndex = 0)then
utmproj.LatHem := hemNorth
else
utmproj.LatHem := hemSouth;

try
utm2geo := TUTM2Geo.Create;
utm2geo.Ellipsoid := ell;
utm2geo.UTMProjection := utmproj;
utm2Geo.UTMCoordinate := utmcoor;
utm2Geo.Compute;
txtLat.Text := format('%11.8f', [utm2geo.GeoCoordinate.Y]);
txtLong.Text := format('%11.8f', [utm2geo.GeoCoordinate.X]);
finally
utm2geo.Free;
end;
end
else  //Geographic to UTM
begin
x := strtofloat(txtLong.Text);
y := strtofloat(txtLat.Text);
geocoor := Coordinate(x, y);

try
geo2utm := TGeo2UTM.Create;
geo2utm.Ellipsoid := ell;
geo2utm.GeoCoordinate := geocoor;
geo2utm.Compute;
utmproj := geo2utm.UTMProjection;
txtN.Text := format('%11.3f', [geo2utm.UTMCoordinate.Y]);
txtE.Text := format('%11.3f', [geo2utm.UTMCoordinate.X]);
txtZoneNo.Text := format('%3d', [utmproj.ZoneNo]);
if (utmproj.LatHem = hemNorth) then
cmbHem.ItemIndex := 0
else
cmbHem.ItemIndex := 1;
finally
geo2utm.Free;
end;
end
end;
  • จากโค๊ดด้านบนซึ่งจะทำหน้าที่คำนวณเมื่อผู้ใช้คลิกที่ปุ่มคำนวณรูปลูกศร ผม declare local variable ไว้ 2 ตัวแปร คือ utm2geo และ geo2utm ตามแต่จะเลือกคำนวณ
  • มีตัวแปรที่สำคัญคือ ell : TEllipsoid เมื่อเริ่มต้นจะดึง object ของ TEllipsoid โดยใช้ชื่อที่ผู้ใช้คลิกที่ combo box “Ellipsoid” เป็นตัว index ในการค้นหา ดังโค๊ด ell := FEllipses.FindEx(cmbEllipsoids.Text);
  • ตัวแปร utmcoor และ geocoor ดึงค่าพิกัดที่ผู้ใช้ป้อนลง text edit
  • คงไม่ยากที่จะเข้าใจเพราะ class ที่ทำหน้าที่คำนวณ แยกออกไปต่างหากแล้ว ดูผลลัพธ์ที่ได้จากการคลิกปุ่ม Example 1 และ Example 2 ตามลำดับ

 

UTM to Geographic.
UTM to Geographic.
Geographic to UTM.
Geographic to UTM.
  • ผมตั้งใจจะรวมไฟล์โปรเจคนี้เป็น zip ไฟล์ แต่บริการแชร์ไฟล์ของ WordPress คือ Box.net ยังปิดบริการชั่วคราวครับ ตอนหน้าถ้าเป็น programming ด้วย Lazarus ผมอยากจะเขียนเรื่อง การคำนวณหาระยะทางและอะซิมัทบนรูปทรงรี (Geodesic distance & Azimuth on Ellipsoid) เมื่อ input เป็นค่าพิกัด Lat/Long ทั้ง 2 จุด (เป็นค่าพิกัด UTM ก็ได้แต่ต้องคำนวณเป็น Geographic ด้วย class ที่ผมเสนอในตอนนี้นั่นเอง)  ผู้อ่านเคยสงสัยบ้างไหมครับว่าเครื่อง GPS เช่น Garmin เมื่อเราป้อน Waypoint ก็คือจุดที่มีค่าพิกัด ผู้ใช้สามารถเลือกป้อนเป็นค่า Latitude,Longitude ก็ได้ แต่ถ้าป้อนเป็นยูทีเอ็มจะสังเกตเห็นว่าเครื่อง GPS จะบังคับให้เราป้อนโซนของยูทีเอ็มเช่น 47P, 47Q อะไรประมาณนี้ (ตัวเลข P,Q เรียกว่า UTM Sub Zone เริ่มจากตัว C ที่ใกล้ขั้วโลกใต้ขึ้นไปหาตัว X ที่ขั้วโลกเหนือ) เครื่อง GPS จะฝังฟังก์ชั่นที่กล่าวนี้เพื่อหาระยะทางและอะซิมัทบน Ellipsoid และก็เช่นเคยสูตรพวกนี้นักคณิตศาสตร์ได้คิดกันมาก่อนแล้วก็มีท่านอื่นปรับปรุงมาเรื่อยๆให้ได้ความถูกต้องเพิ่มขึ้น
UTM sub zone.
UTM sub zone.
  • คำถามคือทำไมไม่หาระยะทางและอะซิมัทจากค่าพิกัดยูทีเอ็มโดยตรง คำตอบคือหาได้ แต่ต้องอยู่บนโซนเดียวกัน แต่ค่าที่คำนวณจะผิดได้ ถ้าไม่มี UTM Zone No กำกับค่าพิกัดมาให้ เครื่อง GPS จึงบังคับให้เราป้อนค่าโชนดังที่ได้กล่าวไปแล้ว

Download sourcecode

  • sourcecode ของโปรแกรมตอนนี้สามารถ download ได้ที่นี่ UTM2LatLongProj.zip

การเขียนโปรแกรมคำนวณการแปลงค่าพิกัดระหว่าง UTM Grid และ Geographic (Lat/Long) ด้วย Lazarus และ Delphi (ตอนที่ 2)

สูตรที่ใช้ในการคำนวณ

  • ผมอ้างอิงมาจาก http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.htm เขียนโดย Steve Dutch กล่าวถึงสูตรที่นำมาใช้คำนวณแปลงค่าพิกัด โดยอ้างถึง U.S. Army Corps of Engineer และ USGS (U.S. Geological Survey Professional) ถ้าเป็นการแปลงจาก Lat/Long ไป UTM เอกสารของ U.S. Army Corps of Engineer ทำได้ดีกว่าชัดเจนกว่า แต่ถ้าเป็นการแปลงพิกัดจาก UTM ไป Lat/Long USGS ทำได้ดีกว่า
  • เราจะใช้สูตรที่ Steve Dutch ได้อ้างถึงและแสดงไว้ในเอกสาร นำมาโค๊ดเป็น Lazarus (Delphi) และที่น่าสนใจคือมีไฟล์ของ Excel สำหรับการคำนวณไว้ให้ดาวน์โหลดได้ด้วย ทำไว้ได้ดีมากครับ อยู่ตรงนี้ http://www.uwgb.edu/dutchs/UsefulData/UTMConversions1.xls

UTM Zone

UTM Zone No
UTM Zone No

GeoEllipsoids Unit

  • เราจะเริ่มด้วย Unit “GeoEllipsoids” (เก็บอยู่ในไฟล์ geoellipsoids.pas ที่ Lazarus พยายามให้เราตั้งชื่อเป็น lower case) เป็นยูนิตที่ declare class รูปแบบของ Ellipsoid ชื่อ TEllipsoid และมีสร้าง collection สำหรับเก็บ Ellipsoid ไว้ด้วยในยูนิตเดียวกัน
      </ul>
      </ul>
      &nbsp;
      <ul>
      <ul>unit GeoEllipsoids;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>{$mode objfpc}{$H+}</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>interface</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>uses</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>Classes, Contnrs, SysUtils;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>type</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>TEllipsoid = class</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>private</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>FEllipName : string;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>FMajorAxis : double;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>FInvFlat : double;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>public</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>property EllipsoidName : string read FEllipName write FEllipName;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>property MajorAxis : double read FMajorAxis write FMajorAxis;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>property InvFlattening : double read FInvFlat write FInvFlat;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>constructor Create ;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>destructor Destroy ; override ;</ul>
      </ul>
      &nbsp;
      <ul>
      <ul>end;</ul>
      </ul>
      TEllipsoidList = class (TStringList)
      private
      protected
      function GetEllipsoid(idx : Integer ) : TEllipsoid ;
      public
      constructor Create ;
      destructor Destroy ; override ;
      procedure AddEx (const Name : string; const MajorAxisA : double; const InvFlat : double);
      function FindEx(const Name : string): TEllipsoid ;
      end;
      
      implementation
      //TEllipsoid
      constructor TEllipsoid.Create;
      begin
      inherited create;
      end;
      
      destructor TEllipsoid.Destroy;
      begin
      inherited Destroy;
      end;
      
      //TEllipsoidList - Collection of TEllipsoid.
      constructor TEllipsoidList.Create ;
      begin
      inherited Create;
      Sorted := True ;
      
      AddEx('WGS84', 6378137, 298.257223563);
      AddEx('GRS80', 6378137, 298.257222101);
      AddEx('WGS72', 6378135, 298.260);
      AddEx('Australian 1965', 6378160, 298.250);
      AddEx('Krasovsky 1940', 6378245, 298.3);
      AddEx('International (1924)', 6378388, 297);
      AddEx('Clake 1880', 6378249.1, 293.460);
      AddEx('Clarke 1866', 6378206.4, 294.980);
      AddEx('Airy 1830', 6377563.4, 299.320);
      AddEx('Bessel 1841', 6377397.2, 299.150);
      AddEx('Everest 1830', 6377276.345, 300.8017);
      AddEx('Hayford 1909', 6378388.0, 296.999362081575);
      AddEx('North American 1927', 6378206.4, 294.978698213898);
      AddEx('NAD 83', 6378137, 298.257223563);
      end ;
      
      destructor TEllipsoidList.Destroy ;
      var
      i : Integer ;
      begin
      for i:= Count -1 downto 0 do
      begin
      if Assigned( Objects[i] ) then
      TEllipsoidList( Objects[i] ).Free ;
      end ;
      inherited Destroy;
      end ;
      
      procedure TEllipsoidList.AddEx(const Name : string ; const MajorAxisA : double; const InvFlat : double);
      var
      e : TEllipsoid ;
      pos : Integer ;
      begin
      if Find(Name, pos ) then
      begin
      if Assigned(Objects[pos]) then
      TEllipsoid(Objects[pos]).Free ;
      end ;
      pos := Add(Name);
      e := TEllipsoid.Create ;
      with e do
      begin
      EllipsoidName := Name;
      MajorAxis := MajorAxisA;
      InvFlattening := InvFlat;
      end ;
      Objects[pos] := e;
      end;
      
      function TEllipsoidList.FindEx(const Name : String): TEllipsoid ;
      var
      pos : Integer ;
      begin
      if Find( Name, pos ) then
      Result := TEllipsoid(Objects[pos])
      else
      Result := nil ;
      end ;
      
      function TEllipsoidList.GetEllipsoid(idx : Integer) : TEllipsoid ;
      begin
      Result := TEllipsoid( Objects[ idx ] );
      end ;
      
      end.
      
      • จากโค๊ตด้านบนจะเห็นผม Declare class ชื่อ TEllipsoid มี property เก็บค่าของรูปทรงรี เนื่องจากค่าของรูปทรงรีที่คนใช้กันเป็นค่ามาตรฐานผมจึงนำ object ของ class “TEllipsoid” มาสร้างเก็บไว้ใน class ที่สืบทอดจาก TStringList ให้ชื่อว่า TEllipsoidList ทำไมต้อง derive จาก TStringList เนื่องจาก TStringList ออกแบบมาให้เก็บชุดของตัวอักษรแต่สามารถเก็บ Object ได้  มี Index ที่สามารถค้นหาได้ด้วยเมธอด Find ความหมายง่ายๆคือเราเอา Object ของ class “TEllipsoid” มายัดใส่ใน list ของ TEllipsoidList(derive มาจาก TStringList) โดยอาศัยชื่อของทรงรี (EllipsoidName) มาเป็นตัว index เอาไว้ค้นหา Object
      • ผมเพิ่มเมธอด FindEx สำหรับค้นหาและดึง object ของทรงรีออกมา และยังเพิ่ม AddEx สำหรับเผื่อไว้ในกรณีต้องการเพิ่มทรงรีอื่นในภายหลังได้

      GeoCompute Unit

      • ต่อไปเป็น Unit ที่ 2 ชื่อ “GeoCompute” (เก็บอยู่ในไฟล์ geocompute.pas)
      unit GeoCompute;
      
      {$mode objfpc}{$H+}
      
      interface
      
      uses
      Classes, SysUtils, GeoEllipsoids, Math;
      
      type
      TZoneHemisphere = (hemEast, hemWest, hemNorth, hemSouth);
      
      TUTMProjection = record
      K0 : double;
      FE : double;
      FN : double;
      ZoneNo : integer;
      CM : double;
      LatHem : TZoneHemisphere;
      Longhem : TZoneHemisphere;
      end;
      
      TCoordinate = record
      X : double;
      Y : double;
      end;
      
      TGeo2UTM = class
      private
      FEllipsoid : TEllipsoid;
      FGeoCoor : TCoordinate;
      FUTMCoor : TCoordinate;
      FUTMProj : TUTMProjection;
      procedure SetGeoCoordinate (const geocoor : TCoordinate);
      procedure SetEllipsoid(const ellipse : TEllipsoid);
      public
      property Ellipsoid : TEllipsoid write SetEllipsoid;
      property GeoCoordinate : TCoordinate write SetGeoCoordinate; //input
      property UTMCoordinate : TCoordinate read FUTMCoor;  //output
      property UTMProjection : TUTMProjection read FUTMProj; //output
      procedure Compute;
      constructor Create;
      destructor Destroy; override;
      end;
      
      //On UTM Grid coordianates we need to know the zone no, latitude hemisphere and
      //longitude hemisphere also. Then we can convert utm coordinates to geographic.
      TUTM2Geo = class
      private
      FEllipsoid : TEllipsoid;
      FGeoCoor : TCoordinate;
      FUTMCoor : TCoordinate;
      FUTMProj : TUTMProjection;
      procedure SetUTMProjection (const utmproj : TUTMProjection);
      procedure SetUTMCoordinate (const utmcoor : TCoordinate);
      procedure SetEllipsoid(const Ellipsoid : TEllipsoid);
      public
      property UTMProjection : TUTMProjection read FUTMProj write SetUTMProjection;
      property Ellipsoid : TEllipsoid write SetEllipsoid;
      property UTMCoordinate : TCoordinate write SetUTMCoordinate;  //input
      property GeoCoordinate : TCoordinate read FGeoCoor; //output
      procedure Compute;
      constructor Create;
      destructor Destroy; override;
      end;
      
      implementation
      
      //TGeo2UTM
      procedure TGeo2UTM.SetEllipsoid(const ellipse : TEllipsoid);
      begin
      FEllipsoid.EllipsoidName := ellipse.EllipsoidName;
      FEllipsoid.MajorAxis := ellipse.MajorAxis;
      FEllipsoid.InvFlattening := ellipse.InvFlattening;
      end;
      
      procedure TGeo2UTM.SetGeoCoordinate(const geocoor : TCoordinate);
      var
      dLat, dLong : double;
      begin
      FGeoCoor := geocoor;
      dLat := FGeoCoor.Y;
      dLong := FGeoCoor.X;
      
      //set hemisphere
      if (dLat &amp;gt;=0) then
      FUTMProj.LatHem := hemNorth
      else
      FUTMProj.LatHem := hemSouth;
      
      if (dLong &amp;gt;=0) then
      FUTMProj.LongHem := hemEast
      else
      FUTMProj.LongHem := hemWest;
      
      //compute zone
      if (FUTMProj.LongHem = hemWest) then
      FUTMProj.ZoneNo := trunc((180 + dLong) / 6) + 1
      else if (FUTMProj.LongHem = hemEast) then
      FUTMProj.ZoneNo := trunc(dLong/6) + 31;
      
      //compute zone
      if (FUTMProj.LatHem = hemNorth) then
      FUTMProj.FN := 0
      else if (FUTMProj.LatHem = hemSouth) then
      FUTMProj.FN := 10000000;
      
      FUTMProj.CM := 6 * FUTMProj.ZoneNo - 183;
      end;
      
      procedure TGeo2UTM.Compute;
      var
      K0, FE, FN : extended;
      lat, long : extended; //in radian.
      a, b, f, e, et2, rm, n, rho, nu, p, cm : extended;
      M, A0, B0, C0, D0, E0 : extended;
      Ki, Kii, Kiii, Kiv, Kv, A6 : extended;
      
      begin
      //Assign to new variable for easy looking at mathematic formula.
      K0 := FUTMProj.K0;
      FE := FUTMProj.FE;
      FN := FUTMProj.FN;
      a := FEllipsoid.MajorAxis;
      f := 1 / FEllipsoid.InvFlattening;
      long := PI/180 * FGeoCoor.X; //convert latitude to radian.
      lat := PI/180 * FGeoCoor.Y;
      cm := PI/180 * FUTMProj.CM;
      p := long - cm; //already in radian.
      
      b := a * (1 - f); //Minor axis;
      
      // =.08 approximately. This is the eccentricity of the earth's elliptical crosssection.
      e := sqrt(1 - b*b / (a*a));
      
      //= .007 approximately. The quantity e' only occurs in even powers so it need only be calculated as e'2
      et2 := power((e * a/b), 2);
      
      n := (a - b) / (a + b);
      
      //This is the radius of curvature of the earth in the meridian plane.
      rho := a * (1 - e*e) / power (1 - power(e*sin(lat),2), 3/2);
      
      //This is the radius of curvature of the earth perpendicular to the meridian plane.
      nu := a / sqrt(1 - sqr(e*sin(lat)));
      
      //=================== compute Meridian Arc===================================
      A0 := a * (1 - n + (5.0/4.0)*(n*n - n*n*n) + (81.0/64.0)*(power(n,4) - power(n,5)));
      B0 := (3*a*n/2) * (1 - n + (7/8)*(n*n - n*n*n) + (55/64)* power(n,4) - power(n,5));
      C0 := (15*a*n*n/16) * (1 - n + (3/4)*(n*n - n*n*n));
      D0 := (35*a*n*n*n/48) * (1 - n + (11/16)*(n*n - n*n*n));
      E0 := (315*a*power(n,4)/512) * (1-n);
      M := A0*lat - B0*sin(2*lat) + C0*sin(4*lat) - D0*sin(6*lat) + E0*sin(8*lat);
      //===========================================================================
      
      //===================Converting Lat/Long to UTM Grid Coordinate==============
      Ki := M * K0;
      Kii := K0 * nu * sin(2*lat) / 4;
      Kiii := (K0 * nu * sin(lat) * power(cos(lat),3) / 24) * ((5 - tan(lat)*tan(lat)
      + 9*et2 * cos(lat)*cos(lat) + 4 * et2*et2 * power(cos(lat),4)));
      Kiv := K0 * nu * cos(lat);
      Kv := (K0 * nu * power(cos(lat),3)/6) * (1 - tan(lat)*tan(lat) + et2*cos(lat)*cos(lat));
      //Now we 'v got Grid UTM Coordinates.
      FUTMCoor.Y := FN + Ki + Kii*p*p + Kiii*power(p,4);
      FUTMCoor.X := FE + Kiv*p + Kv*p*p*p;
      end;
      
      constructor TGeo2UTM.Create;
      begin
      inherited create;
      FUTMProj.K0 := 0.9996;
      FUTMProj.FE := 500000;
      FEllipsoid := TEllipsoid.Create;
      end;
      
      destructor TGeo2UTM.Destroy;
      begin
      FEllipsoid.Free;
      inherited Destroy;
      end;
      
      //TUTM2Geo
      procedure TUTM2Geo.SetEllipsoid(const Ellipsoid : TEllipsoid);
      begin
      FEllipsoid.EllipsoidName := Ellipsoid.EllipsoidName;
      FEllipsoid.MajorAxis := Ellipsoid.MajorAxis;
      FEllipsoid.InvFlattening := Ellipsoid.InvFlattening;
      end;
      
      procedure TUTM2Geo.SetUTMProjection(const utmproj : TUTMProjection);
      begin
      FUTMProj.LongHem := utmproj.LongHem;
      FUTMProj.LatHem := utmproj.LatHem;
      if (utmproj.LatHem = hemNorth) then
      FUTMProj.FN := 0
      else if (utmproj.LatHem = hemSouth) then
      FUTMProj.FN := 10000000;
      
      FUTMProj.CM := 6 * utmproj.ZoneNo - 183;
      
      if (FUTMProj.CM &amp;lt; 0) then
      FUTMProj.Longhem := hemWest
      else
      FUTMProj.Longhem := hemEast;
      
      end;
      
      procedure TUTM2Geo.SetUTMCoordinate(const utmcoor : TCoordinate);
      var
      dN, dE : double;
      begin
      FUTMCoor := utmcoor;
      dN := FGeoCoor.Y;
      dE := FGeoCoor.X;
      end;
      
      procedure TUTM2Geo.Compute;
      var
      K0, FE, FN, a, b, f, e, et2, cm : double;
      mu, M, e1 : double;
      J1, J2, J3, J4 : double;
      C1, T1, R1, N1, D : double;
      fp, Q1, Q2, Q3, Q4, Q5, Q6, Q7 : double;
      lat, long : double;
      x, y : double;
      begin
      //Assign to new variable for easy looking at mathematic formula.
      K0 := FUTMProj.K0;
      FE := FUTMProj.FE;
      FN := FUTMProj.FN;
      a := FEllipsoid.MajorAxis;
      f := 1 / FEllipsoid.InvFlattening;
      x := (FE - FUTMCoor.X);
      if (FUTMProj.LatHem = hemNorth) then
      y := FUTMCoor.Y
      else if (FUTMProj.LatHem = hemSouth) then
      y := (10000000 - FUTMCoor.Y);  //temporary
      
      cm := PI/180 * FUTMProj.CM;
      b := a * (1 - f); //Minor axis;
      
      // =.08 approximately. This is the eccentricity of the earth's elliptical crosssection.
      e := sqrt(1 - b*b / (a*a));
      
      //= .007 approximately. The quantity e' only occurs in even powers so it need only be calculated as e'2
      et2 := power((e * a/b), 2);
      
      M := y / K0; // M is Meridian Arc;
      //Compute footprint Latitude.
      mu := M/(a*(1 - e*e/4 - 3/64*power(e,4) - 5/256*power(e,6)));
      e1 := (1 - sqrt(1 - e*e))/(1 + sqrt(1 - e*e));
      J1 := (3*e1/2 - 27*e1*e1*e1/32);
      J2 := 21*e1*e1/16 - 55*power(e1,4)/32;
      J3 := 151*e1*e1*e1/96;
      J4 := 1097*power(e1,4)/512;
      //Footprint Latitude fp = mu + J1sin(2mu) + J2sin(4mu) + J3sin(6mu) + J4sin(8mu)
      fp := mu + J1*sin(2*mu) + J2*sin(4*mu) + J3*sin(6*mu) + J4 * sin(8*mu);
      
      C1 := et2*cos(fp)*cos(fp);
      T1 := tan(fp)*tan(fp);
      R1 := a*(1-e*e)/power(1-e*e*sin(fp)*sin(fp), 1.5);
      N1 := a / sqrt(1 -e*e*sin(fp)*sin(fp));
      D := x / (N1 * K0);
      //Compute Latitude
      Q1 := N1 * tan(fp)/R1;
      Q2 := D*D/2;
      Q3 := (5 + 3*T1 + 10*C1 - 4*C1*C1-9*et2)*power(D,4)/24;
      Q4 := (61 + 90*T1 + 298*C1 + 45*T1*T1 - 3*C1*C1 - 252*et2)* power(D,6)/720;
      
      Q5 := D;
      Q6 := (1 + 2*T1 + C1) *D*D*D/6;
      Q7 := (5 - 2*C1 + 28*T1 - 3*C1*C1 + 8*et2 + 24*T1*T1)*power(D,5)/120;
      
      //lat
      lat := 180/PI *(fp - Q1*(Q2 + Q3 + Q4));
      
      //long = long0 + (Q5 - Q6 + Q7)/cos(fp)
      long := 180/PI *(CM - (Q5 - Q6 + Q7)/cos(fp));
      FGeoCoor.Y := lat;
      FGeoCoor.X := long;
      end;
      
      constructor TUTM2Geo.Create;
      begin
      inherited create;
      FEllipsoid := TEllipsoid.Create;
      FUTMProj.K0 := 0.9996;
      FUTMProj.FE := 500000;
      end;
      
      destructor TUTM2Geo.Destroy;
      begin
      FEllipsoid.Free;
      inherited Destroy;
      end;
      
      end.
      
      • จากโค๊ดด้านบนผม declare type ที่เป็น record ไว้ 2 แบบ คือ
        1. TUTMProjection เพื่อช่วยกำหนดตำแหน่งของ UTM Zone เช่น Zone No., CM (Central Meridian), FE (False Easting เท่ากับ 500000 เสมอ), FN(False Northing = 0 ถ้าอยู่ด้านเหนือเส้นศูนย์สูตร และเท่ากับ 10000000 ถ้าอยู่ด้านใต้เส้นศูนย์สูตร), K0 (Scale Factor = 0.9996)
        2. TCoordinate เพื่อเก็บค่าพิกัดซึ่งจะมี 2 ค่าคือ Latitude, Longitude หรือ X,Y (UTM)
      • ถัดลงมาอีกผม declare class ไว้ 2 class คือ TUTM2Geo และ TGeo2UTM มาดูรายละเอียด
        1. TGeo2UTM ทำหน้าที่คำนวณค่าพิกัดจาก Geographic ไปเป็น UTM
          • property สำคัญคือ Ellipsoid เราต้อง set โดยการส่ง object ของ class TEllipsoid ที่ผู้ใช้เลือกมาให้
          • property อีกตัวคือ GeoCoordinate เป็นค่าพิกัด Lat, Long ที่เราส่งไปให้เพื่อจะแปลงค่าพิกัดเป็นพิกัดยูทีเอ็ม
          • Compute เป็น method สำคัญที่ทำหน้าที่คำนวณแปลงค่าพิกัด ในเมธอดนี้จะมีสูตรที่ผมอ้างอิงถึงดังที่กล่าวไปแล้ว
          • ส่วนผลลัพธ์ส่งออกมาทาง property UTMCoordinate จะเป็นค่าพิกัดยูทีเอ็มที่เราต้องการ
          • และ property UTMProjection ก็เป็นผลลัพธ์อีกตัวที่ได้จากการคำนวณ ข้อดีของค่าพิกัดในรูป Geographic (Latitude/Longitude) สามารถคำนวณหาได้ว่าอยู่ Zone ไหนของยูทีเอ็ม, หา Central Meridian ได้
        2. TUTM2Geo ทำหน้าที่คำนวณค่าพิกัดจาก UTM ไปเป็น Geographic
          • Ellipsoid property เช่นเดียวกันกับ class TGeo2UTM
          • UTMProjection property ค่าพารามิเตอร์ของยูทีเอ็มที่เราส่งไปให้ เพื่อทำการคำนวณแปลงพิกัดจาก UTM ไปยัง Geographic จำเป็นต้องรู้พารามิเตอร์ของยูทีเอ็ม เช่น Zone No., Hemisphere อยู่เหนือหรือใต้เส้นศูนย์สูตร (ค่าพิกัดยูทีเอ็มค่าพิกัดเดียวกัน อาจจะมีซ้ำกันหลายๆโซน ดังนั้นถ้าบอกพิกัดยูทีเอ็มต้องบอก Zone No. และ Hemisphere ด้วย)
          • UTMCoordinate property เป็นค่าพิกัดยูทีเอ็มที่ต้องการแปลงค่าพิกัดไปยัง Geographic
          • Compute method เช่นเดียวกันเมื่อ input คือ Ellipsoid, UTMprojection และ UTMCoordinate พร้อมก็เรียกใช้ method นี้ได้เลยเพื่อทำการคำนวณ
          • GeoCoordinate property เป็น output คือค่าพิกัด Latitude และ Longitude ที่เราต้องการนั่นเอง
      • ทิ้งท้ายไว้ตรงนี้ครับ ตอนหน้าเป็นตอนสุดท้าย 🙂

การเขียนโปรแกรมคำนวณการแปลงค่าพิกัดระหว่าง UTM Grid และ Geographic (Lat/Long) ด้วย Lazarus และ Delphi (ตอนที่ 1)

ตัวอย่างโปรแกรมแปลงค่าพิกัด GeoCalc และ GeoTrans

  • ผู้ใช้งานด้าน GIS และคนที่ทำงานด้านสำรวจ (Surveying) คงหลีกหนีการแปลงพิกัดระหว่าง UTM Grid และ Geographic (Latitude, Longitude) ได้ยาก ทั้งทางตรงและทางอ้อม ทางตรงได้แก่ ใช้โปรแกรมแปลงพิกัดเช่น GeoCalc (ฟรี แต่ไม่ Opensource) สามารถดาวน์โหลดได้ที่ http://www.geocomp.com.au/geocalc/gcalc420.exe โปรแกรมนี้สามารถคำนวณค่าพิกัดข้าม Datum ไปอีก Datum ได้ (แต่ต้องระวังอีกนิดเพราะโปรแกรม Geocalc มี mistake อยู่ค่า Semi-major axis (a) ของทรงรี Everest 1830 ผิดต้องแก้ไขจาก 6377267.345 เป็น 6377276.345)
  • อีกโปรแกรมคือ GeoTrans ฟรีและ Opensource มี source code ทั้ง Java และ C สามารถ download ได้ที่ http://earth-info.nga.mil/GandG/geotrans/geotrans3.7/master.zip มีให้เลือก download ทั้ง Windows และ Unix (ผมยังไม่เคยนำไปใช้ใน Linux)  GeoTrans เป็นโปรแกรมที่สามารถใช้แปลงค่าพิกัดระหว่าง Datum สนับสนุน Datum & Map Projection ได้หมดทั่วโลก ตัว source code มีคนนำไปแปลงและใช้เช่น แปลงเป็น Delphi ใช้ในโปรแกรม TatukGIS (ไม่ฟรี) ซึ่งเป็น GIS Development Tool Kit มี ActiveX, .NET Winform และ Native VCL ให้เลือกใช้และใช้ได้บนวินโดส์เท่านั้น
  • ส่วนการใช้โปรแกรมแปลงค่าพิกัดที่เป็น Tool ติดมากับโปรแกรมอื่นๆ เช่นบน ERDAS Imagine, Terramodel,  Global Mapper หรือกระทั่งบน Web ก็มีให้เห็นมากพอสมควร

Library สำเร็จรูป “GeographicLib”

  • ถ้าเข้าไปดู source code ของ GeoTrans จะเห็นสูตรที่ใช้แปลงค่าพิกัดเหล่านี้ ยาวเหยียด แยกตามเส้นโครงแผนที่ (Map Projection) ถ้าเกิดจะมีใครพัฒนาโปรแกรมที่เกี่ยวข้อง GIS จาก Ground to top คงต้องศึกษา Library ของ GeoTrans ถ้าไม่คิดจะใช้ Library ของผู้อื่น พูดถึง Library ที่เป็น Opensource ก็ต้อง GeographicLib เขียนโดย Charles Karney ดาวน์โหลดได้ที่ Geographic.zip ตัว GeographicLib เขียนด้วย C++ ตัวนี้ปรับปรุงมาจาก GeoTrans  แต่มีฟังก์ชั่นมากกว่า GeoTrans เสียอีก เช่นการคำนวณเรื่อง Geoid หันมาดู source code ที่เขียนด้วย Delphi ผมหาไม่พบ ต้องยอมรับว่าเปอร์เซนต์ผู้ใช้ Delphi นั้นน้อยน่าใจหาย หลังจาก Borland ล้มหายตายจาก ผมไม่ทราบว่า Developer ของ Delphi จะมีเหลือมากน้อยเท่าใด ส่วน Lazarus ไม่ต้องพูดถึงว่าน้อยขนาดไหน


Datum Transformation Diagram
Datum Transformation Diagram
  • จากไดอะแกรมรูปข้างบนการแปลงค่าพิกัดกริดเช่นจาก UTM Grid (N,E,Z) บน WGS84 => UTM Grid (N,E,Z) บน Indian 1975 datum ได้สองเส้นทางหลัก

    • เส้นทางที่ 1 WGS84( N,E,Z)  ==> WGS84(Lat,Long,Height)  == Molodensky Transformation ==> Indian1975(Lat,Long,Height) ==> Indian1975(N,E,Z)
    • เส้นทางที่ 2 WGS84( N,E,Z)  ==> WGS84(Lat,Long,Height) ==>WGS84(X,Y,Z) ==7,5,3 Parameter Transformation==> Indian1975(X,Y,Z) ==> Indian1975(Lat,Long,Height) ==> Indian1975(N,E,Z)
  • เส้นทางที่ 1 จะสั้นกว่า จากค่าพิกัด Latitude,Long itude คำนวณโดยใช้ Molodensky Transformation หาค่าพิกัด Latitude,Longitude บนอีก datum ได้ ส่วนเส้นทางที่ 2 จะยาวกว่า จากค่าพิกัด Latitude,Longitude จะคำนวณไปหาค่าพิกัดในระบบ Cartesian แล้วใช้ค่าพารามิเตอร์ 3,5 หรือ 7 ค่าในการ Transformation ไปยังระบบ Cartesian บนอีก datum แล้วคำนวณขึ้นไปหา Latitude,Longitude จนถึงค่าพิกัดกริดยูทีเอ็ม ถ้าต้องการ
  • ซึ่งถ้าจะโปรแกรมกันจริงๆ ตามไดอะแกรมด้านบน ก็ใหญ่ขนาด GeographicLib ถ้าจะต้อง support ทุก datum ทุก Map Projection ที่กล่าวไปแล้ว แต่ในตอนนี้ผมขอบีบขอบเขตให้เล็กลง เราจะทำการคำนวณแปลงค่าพิกัดระหว่าง Lat/Long และ UTM Grid บน datum เดียวกันเท่านั้น เช่นคำนวณ Latitude,Longitude ของ WGS 84 ไปเป็นค่าพิกัดยูทีเอ็มกริด (Northing, Easting) บน WGS84 หรือในทางกลับกัน

สิ่งที่ต้องทราบก่อนจะโปรแกรม

  • ขอนำกลับมาที่ datum ก่อนจะไปต่อ datum เป็นพื้นหลักฐานที่แต่ละประเทศต่างก็ใช้แตกต่างกันไป เช่นประเทศในย่านเอเชียตะวันออกเฉียงใต้เช่นพม่า ไทย เขมร ลาว ใช้ Indian 1975 datum มี Spheroid (Ellipsoid) ทรงรีที่เราเรียกว่า Everest 1830 ทรงรีที่เราใช้แทนสัณฐานของโลกจะยุบที่ขั้วโลกเหนือใต้ โป่งออกตรงเส้นศูนย์สูตร สัณฐานของทรงรีจะกำหนดด้วยค่าSemi- major Axis (a) ความยาวของแกนหลัก ค่าการยุบตัว Flattening (f) และความยาวแกนรอง Semi-minor Axis (b) ค่า 3 ค่านี้จะสัมพันธ์กันหมดคือ b = a(1-f) สำหรับ Everest 1830 มีค่า a=6377276.345 , f = 1/300.8017
  • มาถึงยุคของ GPS พื้นหลักฐานของ GPS ในปัจจุบันคือ WGS84 ที่หลายประเทศรวมทั้งไทยเรา หันมาใช้ datum นี้กันมากขึ้น สัณฐานทรงรี WGS94 ค่า a=6378137 ค่า f=1/298.257 223 563

 รูปทรงของทรงรี (ภาพจาก Wikipedia)

  • ทรงรีที่เราใช้กันในโลกนี้มีไม่มาก ไม่น่าปวดหัว มีตัวหลักๆประมาณ 10 กว่าทรงรี
  • ถ้าจะเขียนโปรแกรมให้ใช้ได้ World wide ทั่วโลก สิ่งที่จะปวดหัวก็คือเส้นโครงแผนที่ Map Projection และ Transformation Parameters เพราะมีมากแต่ละประเทศจะใช้ค่าพารามิเตอร์จำนวน 3, 5, 7 เพื่อ Transformation ค่าพิกัดแตกต่างกันไป ตัวอย่างเช่นประเทศแถวตะวันออกกลาง จะใช้ Ellipsoid “International 1924” ใช้เส้นโครงแผนที่ UTM เหมือนกัน แต่จะต่างกันตรงที่ใช้ 3 Parameter Transformation แตกต่างกันไป แต่ถ้าใช้ในประเทศไทยเรา เส้นโครงแผนที่ที่ใช้ก็มีเพียงแต่ Transverse Mercator (TM) ซึ่งเส้นโครงแผนที่ TM ถ้าแบ่งโซนกันแน่นอนก็คือระบบพิกัด Universal Transverse Mercator (UTM) ที่เราคุ้นเคยกันอยู่

เริ่มสร้าง New Project ด้วย Lazarus

  • เปิดโปรแกรม Lazarus คลิกที่เมนู Project > New Project… จะเห็น Dialog เลือก Application คลิกที่ OK
สร้าง New Project
สร้าง New Project
  • จะเห็นฟอร์มเปล่าๆ ดังรูปข้างล่าง ในตอนนี้ผมรัน Lazarus บนวินโดส์
Lazarus รันบนวินโดส์
สร้างโปรเจคใหม่ (Lazarus บนวินโดส์)
  • ก่อนจะไปต่อตอนที่ 2 ผมขอแสดงโปรแกรมขนาดเล็ก ที่เราจะเขียน เพื่อคำนวณค่าพิกัดระหว่าง UTM และ Geographic (Lat/Long) ดังรูปด้านล่าง
โปรแกรมแปลงพิกัดระหว่าง UTM และ Geographic (Lat/Long)
โปรแกรมแปลงพิกัดระหว่าง UTM และ Geographic (Lat/Long)
  • ทิ้งท้ายไว้ตรงนี้ครับ ติดตามตอนต่อไป