Dart & Flutter : เส้นทางขวากหนามกับไลบรารี PROJ แบบเนทีฟบนแอนดรอยด์

เริ่มต้นจากศูนย์ที่ต้นซอยด้วยการพัฒนาแอพด้วยดาร์ทและฟลัตเตอร์ จากที่ยืนหันรีหันขวางแบบยืนงงว่าจะไปทางไหน ตอนนี้ภาษาดาร์ทได้เริ่มซึมซับเข้าสมองมาบ้างแล้ว เริ่มจากคลานตอนนี้พอจะเดินได้แบบเตาะแตะ เคยบอกไปว่าบนฟลัตเตอร์มีไลบรารี Proj4 ชื่อ Proj4Dart แต่มีปัญหาแปลงพิกัดได้คลาดเคลื่อนโดยเฉพาะระบบพิกัดรถไฟความเร็วสูงไทยจีนประมาณ 27 ซม. จนต้องถอยไปตั้งหลักว่าจะเอาไงดีสำหรับการจะใช้ไลบรารี PROJ บนแฟล็ตฟอร์มแอนดรอยด์และไอโอเอส

ทางเลือกแรกใช้ปลั๊กอิน “Chaquopy”

ทางแรกเท่าที่ลองคือเอาไลบรารีของไพทอนมารันบนฟลัตเตอร์ด้วย plug-in ชื่อ Chaquopy (Python for Android) ผมชอบผู้พัฒนาปลั๊กอินนี้ที่สามารถเอาภาษาไพทอนขึ้นมารันได้บนแอนดรอยด์ใช้ Android Studio คือโค้ดจาวาหรือ kotlin แต่ยังใช้ไม่ได้ iOS นะครับ ผู้พัฒนาชื่อมัลคอร์ม สมิธ ล่าสุดได้รับเชิญจากโครงการ Anaconda ให้ไปช่วยโครงการ beeware.org ที่ผมเคยใช้งานอยู่ เนื่องจากต้องการให้คุณสมิธไปช่วยโครงการที่ต้องการเอาภาษาไพทอนไปเขียนแอพบนแอนดรอย์และ iOS ได้ ผมหวังโครงการ beeware.org คงได้เงินทุนและผู้พัฒนาคนใหม่ที่เข้าใจเรื่องนี้อย่างลึกซึ้ง

อนาคตถ้าโครงการ beeware.org ไปได้ดี ผมก็พร้อมกลับไปใช้เขียนแอพด้วยไพทอน เพราะในจักรวาลและโลกเบี้ยวๆใบนี้มีไลบรารีภาษาไพทอนจำนวนมากมายให้เลือกใช้ได้ตามความสะดวก ความพอใจ ตัวอย่างไลบรารีด้าน geodesy สำหรับการแปลงพิกัดต่างๆยกให้ PROJ เป็นเทพ ที่พอร์ทเป็นภาษาไพทอนคือ pyproj และอีกไลบรารีที่ชอบมากเนื่องจากเขียนด้วยภาษาไพทอนเพียวๆคือ pygeodesy สำหรับผมแล้วสองไลบรารีนี้ใช้งานได้ง่าย กินกันไม่ลงเลยทีเดียว

จากที่ลองใช้ปลั๊กอิน Chaquopy ที่ก่อนหน้านี้สักสองเดือนต้องเสียเงิน แต่เมื่อผู้พัฒนาไปเข้าร่วมโครงการ beeware.org ก็มีเงื่อนไขว่าต้องเปิดโค้ด สุดท้ายนี้ผมเลยได้ลองใช้ฟรี

จากการใช้งานสามารถใช้งานได้ดี แต่ปัญหาคือคอขวด เนื่องจากผู้พัฒนาไม่ได้ทำไว้แบบ interactive ทำให้การเรียกใช้งานแต่ละครั้ง ผมเข้าใจว่าต้องไปโหลดภาษาไพทอนใหม่ทุกครั้ง การแปลงพิกัดแต่ละครั้งใช้เวลาประมาณห้าวินาที ซึ่งถ้าเราเอาไปแปลงพิกัดแบบ realtime สมมุติว่าแปลงพิกัดจาก GNSS มือถือที่ค่าพิกัดมีการเปลี่ยนแปลงทุกวินาที คงใช้ไม่ได้ ดูตัวอย่างโค้ดไพทอนที่รันด้วยดาร์ทผ่านปลั๊กอิน “Chaquopy”

import 'package:chaquopy/chaquopy.dart';
import 'package:flutter/material.dart';

//Start of Python code.
var pyCode = ''' 
from pygeodesy.etm import ExactTransverseMercator
from pygeodesy.ellipsoidalVincenty import LatLon
from pygeodesy import Ellipsoid, Transform, Datum, Datums
from pygeodesy.utm import toUtm8, Utm

WGS84_HSR4 = Ellipsoid(a=6378297.0, b=6356911.77779526, f_=298.25722356)
TF_HSR = Transform(tx=0, ty=0, tz=0 , sx=0, sy=0, sz=0, s=0)
DT_HSR4 = Datum(ellipsoid=WGS84_HSR4, transform=TF_HSR)

FALSE_EASTING = 500000

n1=1657193.5292
e1=834934.4417

utm = Utm(zone=47, hemisphere='N', easting=e1, northing=n1, datum=Datums.WGS84)
ll1 = utm.toLatLon(LatLon)
print(ll1.lat, ll1.lon) # send output to Flutter.

p = LatLon(ll1.lat, ll1.lon, datum=Datums.WGS84)
ll2 = p.convertDatum(datum2=DT_HSR4)  #lat lon height
print(ll2.lat, ll2.lon) # send output to Flutter.
q = LatLon(ll2.lat, ll2.lon, datum=DT_HSR4) #lat lon only

ldp = ExactTransverseMercator(datum=DT_HSR4, lon0=102.25, k0=1.0)
u = ldp.forward(ll2.lat, ll2.lon)
e = u.easting + FALSE_EASTING   
n = u.northing
print(n, e) # send output to Flutter.'''; 
//End of python code.
//Start main() of Dart.
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final result = await Chaquopy.executeCode(pyCode);
  String txt = result['textOutputOrError'];
  List coors = txt.split('\n');
  coors.removeWhere((item) => item.isEmpty); //remove empty.
  coors = coors.toSet().toList(); //remove duplates if any.
  List ll1 = coors[0].split(' ');
  List ll2 = coors[1].split(' ');
  List ne2 = coors[2].split(' ');
  double lat1 = double.parse(ll1[0]);
  double lon1 = double.parse(ll1[1]);
  double n2 = double.parse(ne2[0]);
  double e2 = double.parse(ne2[1]);
  double lat2 = double.parse(ll2[0]);
  double lon2 = double.parse(ll2[1]);
  debugPrint('lat2: $lat2');
  debugPrint('lon2: $lon2');
  debugPrint('n2: $n2');
  debugPrint('e2: $e2');
}

ทางเลือกที่สองคอมไพล์โค้ด PROJ ใช้แบบเนทีฟ

ทางเลือกที่สอง คือนำโค้ด C++ ของโครงการ PROJ ไปคอมไพล์เป็นเนทีฟ คือคอมไพล์ให้เป็นไลบรารีแอนดรอยด์หรือ iOS โดยตรง (libproj.so) สำหรับสถาปัตยกรรม Arm ผมคิดอยู่นานมากกว่าจะตัดสินใจลองดู เพราะว่าหนึ่งนั้นไม่กระดิกภาษา C++ อย่างที่สองการคอมไพล์ด้วยเครื่องมือหรือทูลส์ CMake ก็ไม่เคยใช้เลย พยายามค้นหาว่ามีคนเคยทำไว้ไหม เรียกว่า pre-built พอมีบ้างประปราย แต่นานมาก ตัวล่าสุดที่คนเคยทำไว้ประมาณ 4 ปีที่แล้ว ไปลองเอามาใช้ปรากฎว่าใช้งานได้ แต่ข้อเสียคือจะไม่ได้ใช้ไลบรารี PROJ แบบล่าสุด

ลองไปค้นหาการคอมไพล์ให้เป็นเนทีฟ ปรากฎว่าไปเจอของโครงการ OSGeo gdal ที่เขียนสคริปต์ไว้ ปกติเขาจะทำไว้สำหรับการคอมไพล์เป็นเนทีฟสำหรับ gdal แต่ในสคริปต์มีพ่วง PROJ เข้าไปด้วย (เนื่องจากการแปลงพิกัดต่าง gdal เรียกใช้ PROJ) แต่ในสคริปต์เองก็ไปไม่สุดทาง ไม่สามารถคอมไพล์ gdal จนสุดทางได้ เพียงแต่สามารถคอมไพล์ PROJ ได้แค่นั้นผมพอใจแล้วเพราะไม่ได้ใช้ gdal

#!/bin/sh

set -e

apt-get update -y

# pkg-config sqlite3 for proj compilation
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    wget unzip ccache curl ca-certificates \
    pkg-config make binutils sqlite3 \
    automake

cd "$WORK_DIR"

if test -f "$WORK_DIR/ccache.tar.gz"; then
    echo "Restoring ccache..."
    (cd $HOME && tar xzf "$WORK_DIR/ccache.tar.gz")
fi

# We need a recent cmake for recent NDK versions
wget -q https://github.com/Kitware/CMake/releases/download/v3.22.3/cmake-3.22.3-linux-x86_64.tar.gz
tar xzf cmake-3.22.3-linux-x86_64.tar.gz
export PATH=$PWD/cmake-3.22.3-linux-x86_64/bin:$PATH

# Download Android NDK
wget -q https://dl.google.com/android/repository/android-ndk-r23b-linux.zip
unzip -q android-ndk-r23b-linux.zip

export ANDROID_NDK=$PWD/android-ndk-r23b
export NDK_TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64

ccache -M 1G
ccache -s

# build sqlite3
wget -q https://sqlite.org/2022/sqlite-autoconf-3370200.tar.gz
tar xzf sqlite-autoconf-3370200.tar.gz
cd sqlite-autoconf-3370200
CC="ccache $NDK_TOOLCHAIN/bin/aarch64-linux-android24-clang" ./configure \
  --prefix=/tmp/install --host=aarch64-linux-android24
make -j3
make install
cd ..

# Build proj
wget -q https://download.osgeo.org/proj/proj-9.0.0.tar.gz
tar xzf proj-9.0.0.tar.gz
cd proj-9.0.0
mkdir build
cd build
# See later comment in GDAL build section about MAKE_FIND_ROOT_PATH_MODE_INCLUDE, CMAKE_FIND_ROOT_PATH_MODE_LIBRARY
cmake .. \
  -DUSE_CCACHE=ON \
  -DENABLE_TIFF=OFF -DENABLE_CURL=OFF -DBUILD_APPS=OFF -DBUILD_TESTING=OFF \
  -DCMAKE_INSTALL_PREFIX=/tmp/install \
  -DCMAKE_SYSTEM_NAME=Android \
  -DCMAKE_ANDROID_NDK=$ANDROID_NDK \
  -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \
  -DCMAKE_SYSTEM_VERSION=24 \
  "-DCMAKE_PREFIX_PATH=/tmp/install;$NDK_TOOLCHAIN/sysroot/usr/" \
  -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER \
  -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER \
  -DEXE_SQLITE3=/usr/bin/sqlite3
make -j3
make install
cd ../..

# Build GDAL
mkdir build_android_cmake
cd build_android_cmake

# PKG_CONFIG_LIBDIR, CMAKE_FIND_ROOT_PATH_MODE_INCLUDE, CMAKE_FIND_ROOT_PATH_MODE_LIBRARY, CMAKE_FIND_USE_CMAKE_SYSTEM_PATH
# are needed because we don't install dependencies (PROJ, SQLite3) in the NDK sysroot
# This is definitely not the most idiomatic way of proceeding...
PKG_CONFIG_LIBDIR=/tmp/install/lib/pkgconfig cmake .. \
 -DUSE_CCACHE=ON \
 -DCMAKE_INSTALL_PREFIX=/tmp/install \
 -DCMAKE_SYSTEM_NAME=Android \
 -DCMAKE_ANDROID_NDK=$ANDROID_NDK \
 -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \
 -DCMAKE_SYSTEM_VERSION=24 \
 "-DCMAKE_PREFIX_PATH=/tmp/install;$NDK_TOOLCHAIN/sysroot/usr/" \
 -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=NEVER \
 -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER \
 -DCMAKE_FIND_USE_CMAKE_SYSTEM_PATH=NO \
 -DSFCGAL_CONFIG=disabled \
 -DHDF5_C_COMPILER_EXECUTABLE=disabled \
 -DHDF5_CXX_COMPILER_EXECUTABLE=disabled
make -j3
make install
cd ..

ccache -s

echo "Saving ccache..."
rm -f "$WORK_DIR/ccache.tar.gz"
(cd $HOME && tar czf "$WORK_DIR/ccache.tar.gz" .ccache)

สถาปัตยกรรมของ Arm

arm64-v8a คือสถาปัตยกรรมของอาร์มที่ซีพียูเป็น 64 บิต ส่วน armeabi-v7a เป็นรุ่นก่อนหน้านี้ซีพียูเป็น 32 บิต ผมเข้าใจว่าโทรศัพท์มือถือที่เราๆท่านๆใช้กันอยู่น่าจะเป็น 64 บิตกันหมดแล้ว แล้ว x86 และ x86_64 คืออะไร ตอนแรกก็งงๆเพราะยังไมได้อ่านเอกสาร ที่จริง x86 มันเป็นอีมูเลเตอร์ (เครื่องจำลอง) แบบ 32 บิต สำหรับโทรศัพท์แอนดรอยด์ ที่ผู้พัฒนาใช้รันแอพบนพีซีทั้งหลายทั้งวินโดส์และลีนุกซ์ ผ่าน Android Studio และผมก็ใช้งานอยู่ผ่านฟลัตเตอร์ ส่วน x86_64 ก็เช่นกันเพียงแต่เป็นแบบ 64 บิต

คอมไพล์และบิวท์ด้วยสคริปต์

สำหรับ dependency ของไลบรารี PROJ นั้นต้องการลิ๊งค์กับ libsqlite3.so เป็นอย่างน้อยเป็นภาคบังคับ เพราะกลไกทำงานของ PROJ ต้องอ่านไฟล์ proj.db ที่เป็นไฟล์ฐานข้อมูลของ sqlite ในช่วงเริ่มต้น ดังนั้นจึงเป็นการบังคับ ส่วนการเลือกคอมไพล์และลิ๊งค์ไลบรารี libcurl, libtiff สามารถเลือกเปิดหรือปิดได้ ผมเลือกปิดเพราะยังไม่ต้องการใช้ tiff ในขณะนี้ (เป็นข้ออ้างในชีวิตจริงได้ลองเปิดมาทั้งหมดแล้วแต่คอมไพล์และลิ๊งค์ไม่ผ่าน 🙂 ต้องการไลบรารีอื่นเป็นกระตั๊กๆเช่น libcurl, libssl, libzip โหดมาก)

หมายเหตุไฟล์ tiff เอามาเก็บ geoid ซึ่งผมยังใช้รูปแบบเป็น gtx (NOAA/NGS‘s VDatum) ทางผู้พัฒนาไลบรารี PROJ ต้องการเป็นทางเลือกให้อ่าน geoid grid จากไฟล์ tiff ที่เก็บไว้บนคลาวด์ ดังนั้นจึงต้องเลือก libcurl เพื่อมาช่วยในการโอนถ่ายข้อมูลและเข้ารหัส

สคริปต์ที่ใช้ต้องรันบนลีนุกซ์เท่านั้น ผมใช้ ubuntu การทำงานของสคริปต์จะไปดาวน์โหลดซอร์สโค้ดของ CMake, Android-NDK ของ Sqlite และ PROJ มาให้ จากนั้นจะทำการคอมไพล์และบิวท์ให้กับ Sqlite ก่อน ถ้าสำเร็จจะไปต่อด้วย PROJ สคริปต์นี้มาจอดที่ PROJ และคอมไพล์ gdal ได้จนสุดแต่บิวต์ขั้นสุดท้ายไม่ได้ ไม่เป็นไรเพราะผมไม่ต้องการ gdal

กำหนดปลายทาง (Target Platform)

ในสคริปต์ให้ดูบล็อคการคอมไพล์ sqlite3 จะยังไม่ได้ใช้ CMake กำหนดสถาปัตยกรรมด้วยคีย์เวิร์ด –host=aarch64-linux-android24 และคอมไพล์ด้วย aarch64-linux-android24-clang (ตัว aarch64-linux-android เทียบเท่ากับ arm64-v8a)

CC="ccache $NDK_TOOLCHAIN/bin/aarch64-linux-android24-clang" ./configure \
  --prefix=/tmp/install --host=aarch64-linux-android24

การคอมไพล์ PROJ ด้วย CMake จะมีพารามิเตอร์ DCMAKE_ANDROID_ARCH_ABI=arm64-v8a

เปลี่ยนสถาปัตยกรรม

บรรทัดนี้จะระบุสถาปัตยกรรมอาร์ม ซึ่งจะเปลี่ยนเป็น armeabi-v7a, x86 หรือ x86_64 ได้ แก้บล็อค sqlite3 ดังนี้

# for armeabi-v7a
CC="ccache $NDK_TOOLCHAIN/bin/armv7a-linux-androideabi24-clang" ./configure \
  --prefix=/tmp/install --host=armv7a-linux-androideabi24

# for x86
CC="ccache $NDK_TOOLCHAIN/bin/i686-linux-android24-clang" ./configure \
  --prefix=/tmp/install --host=i686-linux-android24
  
 # for x86_64
 CC="ccache $NDK_TOOLCHAIN/bin/x86_64-linux-android24-clang" ./configure \
  --prefix=/tmp/install --host=x86_64-linux-android24

สำหรับบล็อค PROJ แก้ง่ายๆ

# for armeabi-v7a
 -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a \

#for x86
 -DCMAKE_ANDROID_ARCH_ABI=x86 \

#for x86_64
 -DCMAKE_ANDROID_ARCH_ABI=x86_64 \

ไลบรารีที่ได้จากการคอมไพล์และบิวท์ ตามไป copy ไฟล์ได้ libsqlite3.so และ libproj.so ที่ /tmp/install/lib

สุดท้ายเราจะได้ไฟล์ไลบรารี libsqlite3.so และ libproj.so จำนวน 4 ชุด เรียงตามสถาปัตยกรรม ปัญหาที่ผมเจอเพราะเป็นมือใหม่คือจะ copy ไฟล์เหล่านี้ไปไว้ที่ไหนในไดเรคทอรีของ project ผมเสียเวลาเป็นวันๆเหมือนกัน วิธีการแก้ปัญหาส่วนใหญ่ได้จากค้นหาในอินเทอร์เน็ตเพราะที่ผมเจอ คนอื่นเจอมาก่อนทั้งนั้น สำหรับแอนดรอยด์ให้ก็อปปี้ไฟล์ไปไว้ที่ project/android/src/main/jniLibs และแยกย่อยไปตามสถาปัตยกรรมเช่น arm64-v8a, armeabi-v7a และ x86, x86_64 แค่นี้ เมื่อคอมไพล์ฟลัตเตอร์ ตัวบิวท์จะสามารถหาไฟล์ไลบรารีของเราเจอนำไปสร้างเป็นไฟล์ apk

ทางเลือกที่สามเอาโค้ด PROJ มาคอมไพล์ด้วย Android Studio และ XCode

ทางเลือกนี้น่าดีที่สุดแต่ก็โหดที่สุด เพราะ PROJ เป็นโครงการใหญ่มี source code มากมาย ทางเลือกนี้คือนำ source code มาใส่ในฟลัตเตอร์โดยตรงแล้ว config ให้ฝั่งแอนดรอยด์สามารถคอมไพล์ด้วยเครื่องมือ NDK ส่วนฝั่ง IOS ก็คอมไพล์ด้วย XCode วิธีการนี้ผมทำไม่ได้เพราะมือไม่ถึง

อัพเดทโครงงาน Thai Easy Geo

เมื่อนำไลบรารี PROJ มาใช้ในโครงงาน Thai Easy Geo ก็สามารถใช้ได้ดี รวดเร็ว แต่ความงุ่มง่ามจะเกิดขึ้นตอนเขียน interface กับฟังก์ชั่นของ PROJ ผ่านทางไฟล์เฮดเดอร์ proj.h เยิ่นเย้อกว่าจะได้สิ่งที่ต้องการ แต่ที่ได้มาคือความเร็ว ไม่มีอาการสะดุดเลยแม้แต่น้อย ผมลองใช้ไลบรารีในการแปลงพิกัดข้ามพื้นหลักฐานประมาณ 50 จุด มีอาการหน่วงเล็กน้อยพอรอได้ไม่หงุดหงิด สถานะล่าสุดของโครงงานก็ดังรูปแกลลอรีถัดไปครับ

Thai Easy Geo คงได้ออก Google play store แน่นอนครับต้นปีหน้า 2023 ฝากอุดหนุนด้วยครับ

สำหรับรุ่นบน iOS คงต้องรอไปไม่มีกำหนด ผมไม่สามารถเอาไลบรารี PROJ ไปรันได้ทั้งๆที่ทำเป็นเฟรมเวิร์คให้ก็แล้ว รันแบบไลบรารีโดยตรงก็แล้วยังไม่ได้ มือไม่ถึงเพราะไม่เคยใช้ XCode มาก่อน (ฟลัตเตอร์เมื่อมารันบน MacOS จะสร้างโค้ดแล้วไปเรียก XCode คอมไพล์และบิวท์แอพอีกทีต่อ ส่วนฝั่งแอนดรอยด์ก็เรียกใช้เครื่องมือของ Android Studio) ส่วนทางเลือกที่สามที่ผมกล่าวไปแล้วคือเอา source code ของ PROJ ทั้งกระบิมาคอมไพล์ด้วย XCode แต่ก็จนปัญญาเพราะตั้ง config ไม่ได้ พยายามหาที่คนทำไว้ใน Cocoapods แต่ก็เก่าเต็มทีเลยไม่ได้เอามาใช้ โปรดติดตามบทความตอนต่อไปครับ

2 thoughts on “Dart & Flutter : เส้นทางขวากหนามกับไลบรารี PROJ แบบเนทีฟบนแอนดรอยด์”

Leave a Reply

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