Chào anh em Viblo! 👋
Ở hai bài viết trước, chúng ta đã cùng nhau mổ xẻ toán học của thuật toán Point-in-Polygon (PIP) và tuyệt chiêu tăng tốc query bằng cách Lọc theo BBox. Chúng ta cũng nhắc nhiều đến PostGIS như một "vị thần" xử lý dữ liệu không gian dưới Database.
Thế nhưng, hãy đặt mình vào một kịch bản thực tế khác: Hệ thống của bạn viết theo kiến trúc Serverless (AWS Lambda), hoặc bạn đang làm một worker xử lý dữ liệu GPS thời gian thực từ các thiết bị IoT bắn về liên tục. Bạn cần tính khoảng cách giữa các điểm, tạo vùng đệm (buffer zone), hoặc cắt ghép các đa giác ngay trên tầng Application (In-memory) trước khi quyết định ghi cái gì vào DB. Nếu cứ mỗi phép tính nhỏ nhặt như vậy bạn lại phải làm một lượt gọi mạng (network call) xuống PostGIS dưới Database, độ trễ (latency) của hệ thống sẽ tăng vọt, và tiền hóa đơn cloud cuối tháng sẽ khiến sếp "bật ngửa".
Giải pháp giải cứu anh em lúc này chính là Turf.js – một thư viện xử lý hình học không gian (Geospatial analysis) viết bằng JavaScript thuần chủng, chạy mượt mà ở cả Browser lẫn Node.js.
Hôm nay, hãy cùng mình đưa Turf.js vào kho vũ khí Node.js/TypeScript để xử lý bản đồ một cách thanh thoát nhất nhé!
1. Triết Lý Của Turf.js: Tất Cả Là GeoJSON
Điểm tuyệt vời nhất của Turf.js là nó không tự đẻ ra một chuẩn dữ liệu quái dị nào cả. Toàn bộ input và output của Turf.js đều tuân thủ 100% chuẩn GeoJSON (chuẩn định dạng dữ liệu địa lý dựa trên nền JSON, được hỗ trợ bởi hầu hết các bản đồ lớn như Google Maps, Mapbox, Leaflet).
Một thực thể dữ liệu trong Turf sẽ có cấu trúc quen thuộc như sau:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [106.6601, 10.7626] // [Kinh độ, Vĩ độ] - Luôn là Longitude trước, Latitude sau!
},
"properties": {
"name": "Quận 10, TP.HCM"
}
}
2. Bộ Ba "Tuyệt Chiêu" Thực Chiến Với Turf.js
Thay vì phải cài nguyên một package turf khổng lồ, kể từ phiên bản v6+, Turf đã được module hóa. Bạn cần tính năng nào thì chỉ việc cài đúng package con của tính năng đó, giúp tối ưu hóa dung lượng bộ nhớ tuyệt đối.
Tuyệt chiêu 1: Tính khoảng cách đường chim bay siêu tốc (@turf/distance)
Bài toán: Bạn có tọa độ của Khách hàng và tọa độ của Cửa hàng, cần tính xem khoảng cách giữa 2 điểm này là bao nhiêu km theo công thức nửa đường tròn (Haversine formula) để ước tính chi phí.
import distance from '@turf/distance';
import { point } from '@turf/helpers';
const pointA = point([106.6601, 10.7626]); // Cửa hàng
const pointB = point([106.6944, 10.7732]); // Khách hàng
const options = { units: 'kilometers' as const };
const khongCach = distance(pointA, pointB, options);
console.log(`Khoảng cách đường chim bay: ${khongCach.toFixed(2)} km`);
// 👉 Kết quả: ~3.95 km (Tính toán in-memory chỉ mất vài micro-giây!)
Tuyệt chiêu 2: "Vả" Bug Point-in-Polygon trong 1 nốt nhạc (@turf/boolean-point-in-polygon)
Đừng tự viết thuật toán Ray Casting bắn tia phức tạp như bài viết trước nữa anh em ơi, Turf chuẩn hóa nó thành một hàm nhận diện đúng/sai cực kỳ sạch sẽ:
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import { point, polygon } from '@turf/helpers';
// Định nghĩa một vùng đa giác giao hàng
const deliveryZone = polygon([[
[106.65, 10.75],
[106.68, 10.75],
[106.68, 10.78],
[106.65, 10.78],
[106.65, 10.75] // Điểm cuối phải trùng điểm đầu để đóng đa giác!
]]);
const userLocation = point([106.6601, 10.7626]);
const isInside = booleanPointInPolygon(userLocation, deliveryZone);
console.log(`Shipper có được giao hàng không? ${isInside ? 'CÓ' : 'KHÔNG'}`);
Tuyệt chiêu 3: Tạo vùng Geofencing bán kính xung quanh (@turf/buffer)
Bài toán: Từ vị trí của một tài xế xe tải, hãy tạo ra một vùng tròn có bán kính 5 km bao quanh tài xế đó (Buffer zone), để nếu tài xế đi lệch ra khỏi vùng này thì hệ thống tự động bắn cảnh báo về trung tâm điều hành.
import buffer from '@turf/buffer';
import { point } from '@turf/helpers';
const centerPoint = point([106.6601, 10.7626]);
// Tạo một đa giác bao quanh điểm tâm với bán kính 5km
const bufferedZone = buffer(centerPoint, 5, { units: 'kilometers' });
console.log(JSON.stringify(bufferedZone));
// Trả về một đối tượng Polygon GeoJSON hoàn chỉnh, có thể ném thẳng lên map để vẽ đồ họa
3. Những "Vết Sẹo" Thực Chiến Cần Né Khi Dùng Turf.js
Mặc dù Turf.js cực kỳ mạnh mẽ và tiện lợi, nhưng khi đưa nó vào các hệ thống Node.js có High-Traffic, anh em bắt buộc phải nằm lòng 2 lưu ý xương máu sau:
Cạm bẫy 1: Kẻ hủy diệt Event Loop (CPU-Bound Task) Node.js vận hành trên cơ chế Single-Threaded (Đơn luồng). Turf.js tính toán hình học 100% bằng JavaScript chạy trên chính cái Main Thread đó.
- Nếu bạn bắt Turf tính toán các phép tính cực kỳ nặng – ví dụ: Tìm điểm giao nhau (@turf/intersect) giữa hai đa giác ranh giới quốc gia có chứa hàng vạn đỉnh phức tạp – CPU sẽ bị khóa cứng (Blocked) trong vài giây để giải toán. Toàn bộ các Request HTTP khác của user vào server sẽ bị nghẽn mạch (Timeout).
- Bốc thuốc: Đối với các hình học quá phức tạp, hãy đẩy nó xuống cho PostGIS xử lý (vì PostGIS viết bằng C, chạy đa luồng độc lập với Node.js). Hoặc nếu bắt buộc phải làm ở Node.js, hãy tách tác vụ đó ra chạy ở một Worker Thread riêng biệt để giải phóng Main Thread.
Cạm bẫy 2: Lỗi ngược ngạo tọa độ [Vĩ độ, Kinh độ]
Đây là con bug khiến nhiều anh em "trầm cảm" nhất. Ở trường học, chúng ta quen đọc miệng là Vĩ độ trước, Kinh độ sau (Latitude, Longitude). Tuy nhiên, chuẩn GeoJSON của thế giới (và cũng là chuẩn của Turf.js) quy định nghiêm ngặt: Kinh độ trước, Vĩ độ sau [Longitude, Latitude] (tương ứng với trục [X, Y] trong hệ tọa độ toán học).
Chỉ cần bạn vô tình truyền ngược [lat, lng], Turf vẫn chạy, không báo lỗi, nhưng vị trí tính toán của bạn sẽ bị bay sang tận châu Phi hoặc Nam Cực, dẫn đến toàn bộ logic hệ thống bị sai lệch hoàn toàn.
Đúc Kết Lại
Turf.js là vị cứu tinh giúp mang toàn bộ tư duy xử lý không gian của các hệ thống GIS đắt đỏ vào trong những dòng code JavaScript/TypeScript gọn nhẹ. Nó giúp ứng dụng của bạn tự chủ động tính toán, giảm tải tối đa cho Database, cực kỳ phù hợp cho các kiến trúc Microservices và Serverless hiện đại.
Hệ thống của anh em có đang phải xử lý nhiều dữ liệu bản đồ, GPS không và anh em đang dùng giải pháp gì để tính toán in-memory?