ofs-object
v0.1.8
Published
Framework-agnostic TypeScript Library for GIS Map Tools (Data Adapter, Draw Tools, Legend & Thematic Symbology, Map Adapters, Web CLI & AI Bridge)
Maintainers
Readme
OFSObject (Map Tools) 🗺️📐
OFSObject (ofs-object) คือไลบรารี TypeScript แบบ Framework-Agnostic สำหรับพัฒนาระบบแผนที่และเครื่องมือ GIS/CAD ขั้นสูง ออกแบบมาให้ทำงานร่วมกับได้ทุกเฟรมเวิร์ก (Angular, React, Vue, Svelte หรือ Vanilla JavaScript) โดยไม่มีข้อผูกมัดกับเฟรมเวิร์กใดเฟรมเวิร์กหนึ่ง
ไลบรารีนี้รวม 5 เสาหลักสำหรับงานแผนที่สมัยใหม่:
- Data Adapter (
OFSDataSource): ศูนย์กลางจัดการข้อมูลแผนที่ (Single Source of Truth) รองรับ Layer, FeatureCollection, Spatial Query, Turf.js Analytics และระบบ Undo/Redo Transaction - Draw & CAD Tools (
OFSDrawTools,CADOperations): เครื่องมือวาดรูปทรงเรขาคณิต (Point, Line, Polygon, Circle, Rectangle), ระบบช่วยจับวัตถุ AutoCAD OSnap Engine (Vertex, Midpoint, Edge), Ortho Mode (F8), Interactive Grips Editor (ดัดจุด/เพิ่มจุด/ย้ายแปลง/ลบจุด) และคำสั่ง CAD ขั้นสูง (Split, Connect, Offset, Rotate, Scale, Explode, Fillet, Bezier Smooth) - Legend & Thematic Symbology (
OFSLegendTools,OFSThematicEngine,LegendWidget): จัดการลำดับ Layer, ความโปร่งใส (Opacity), และระบบสร้างแผนที่เฉพาะเรื่องอัตโนมัติ (Simple, Categorical, Graduated: Quantile, Equal Interval, Jenks Natural Breaks) แปลงเป็น Mapbox/MapLibre GL Expression พร้อม Drop-in DOM Widget - Map Adapter (
IMapAdapter): สถาปัตยกรรม Abstraction กลางรองรับทั้ง MapLibre GL JS และ Mapbox GL JS สลับ Map Engine ได้อย่างราบรื่น - Web CLI & AI Bridge (
OFSCommandEngine,OFSWebConsoleWidget,OFSAiBridge): ระบบ Pipeline สั่งการแบบ AutoCAD Command Line (รองรับคำสั่งหลายขั้นตอนและการรอรับ Input จากแผนที่) พร้อม DOM Console UI และ AI Bridge แปลงคำสั่งธรรมชาติของ LLM (Gemini, GPT) เป็น Tool Calling พร้อมส่ง Feedback Loop กลับ
สารบัญ (Table of Contents)
การติดตั้ง (Installation)
สถานะ: beta — เผยแพร่ภายใต้ tag
nextยังไม่ผ่านการทดสอบด้วยการใช้งานจริงในเบราว์เซอร์ครบทุกส่วน (ดูหัวข้อ "สิ่งที่ต้องทดสอบใน Browser" ในROADMAP.md) API อาจเปลี่ยนได้ในรุ่นถัดไป
npm install ofs-object@nextPeer Dependencies
เนื่องจากไลบรารีออกแบบให้ Framework-Agnostic จึงเปิดให้เลือกใช้ Map Engine ที่เหมาะสมกับโปรเจกต์ของคุณ:
# สำหรับ MapLibre GL JS (ฟรี, Open-source แนะนำ) — รองรับ v6 ขึ้นไป
npm install maplibre-gl
# หรือสำหรับ Mapbox GL JS (Commercial) — รองรับ v3 ขึ้นไป
npm install mapbox-glMapLibre v6 เป็น ES module ล้วน (ไม่มี CommonJS) ถ้าจะใช้
require()ใน Node ต้องใช้ MapLibre v5 หรือใช้ Mapbox แทน — ผ่าน bundler (Angular, Vite, webpack) ใช้ได้ตามปกติ
⚠️ ต้องส่ง Map Engine เข้ามาเอง
ไลบรารีนี้ไม่ import map engine ตัวไหนเลย — เพราะถ้า import ตัวใดตัวหนึ่งไว้ มันจะกลายเป็น
dependency บังคับของทุกคน คนที่ใช้ Mapbox และไม่ได้ลง maplibre-gl จะ build ไม่ผ่านด้วย
Can't resolve 'maplibre-gl' ตั้งแต่ยังไม่ได้รันโค้ด
ดังนั้นตอนให้ init() สร้างแผนที่ ต้องส่ง library เข้ามาด้วย:
// MapLibre v6 ไม่มี default export → ใช้ namespace import
import * as maplibregl from 'maplibre-gl';
const ofs = new OFSObject({ mapOptions: { maplibregl } });
await ofs.init('map', { center: [100.5, 13.7], zoom: 12 });// Mapbox
import mapboxgl from 'mapbox-gl';
const ofs = new OFSObject({
mapAdapter: new MapboxAdapter(),
mapOptions: { mapboxgl, accessToken: 'pk....' },
});
await ofs.init('map');หรือโหลดจาก <script> tag ก็ได้ ไลบรารีจะอ่านจาก window.maplibregl / window.mapboxgl ให้เอง
ถ้าแอปสร้าง Map เอง (แนะนำสำหรับ Angular/React/Vue — ดูหัวข้อด้านล่าง) แล้วส่งเข้ามาทาง
new OFSObject({ map })ไม่ต้องส่ง engine เลย เพราะไลบรารีไม่ต้องสร้างแผนที่
Stylesheet
Widget ที่ไลบรารี mount ให้ (Command Console, Legend, แผง Symbology, ตาราง Attribute, แผง Topology) ต้องการไฟล์ CSS หนึ่งไฟล์:
@import 'maplibre-gl/dist/maplibre-gl.css';
@import 'ofs-object/styles.css';Angular ใส่ใน angular.json → styles:
"styles": [
"node_modules/maplibre-gl/dist/maplibre-gl.css",
"node_modules/ofs-object/dist/ofs-object.css",
"src/styles.css"
]ไฟล์นี้ไม่มี CSS reset, ไม่ตั้งค่า body และไม่ดึงเว็บฟอนต์ — มีแต่สไตล์ของ widget เท่านั้น สีทั้งหมดเป็น custom property ที่เปลี่ยนทับได้:
:root {
--ofs-accent-blue: #2563eb;
--ofs-text-primary: #111827;
}ติดตั้งจาก Git (สำหรับรุ่นที่ยังไม่ publish)
npm install git+ssh://[email protected]/<user>/OnFarmObject.git#mainprepare script จะ build dist/ ให้อัตโนมัติตอนติดตั้ง (npm ลง devDependencies ชั่วคราวเพื่อ build แล้วตัดทิ้ง) — ต้องใช้ --force เมื่ออัปเดต เพราะ npm cache แยกตาม commit
แพ็กเกจที่เผยแพร่มีเฉพาะ
dist/(bundle +index.d.ts+ofs-object.css) ไม่มีซอร์ส TypeScript
เริ่มต้นใช้งานด่วน (Quick Start)
สร้างระบบแผนที่พร้อมเครื่องมือครบวงจรได้ในไม่กี่บรรทัดผ่านคลาส OFSObject:
import { OFSObject, MapLibreAdapter, LegendWidget } from 'ofs-object';
import * as maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
async function initMap() {
// 1. สร้าง Instance หลักของ OFSObject (ใช้ MapLibre เป็นค่าเริ่มต้น)
// ต้องส่ง maplibregl เข้ามา เพราะไลบรารีไม่ import map engine เอง
const ofs = new OFSObject({
mapAdapter: new MapLibreAdapter(),
mapOptions: { maplibregl },
});
// 2. กำหนดแผนที่ลงบน HTML Container Element id="map"
await ofs.init('map', {
center: [100.5018, 13.7563], // กรุงเทพฯ [lng, lat]
zoom: 12,
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
});
// 3. เพิ่มเลเยอร์ข้อมูลแปลงที่ดิน (Polygon)
const layer = ofs.dataSource.addLayer(
{
id: 'parcels',
name: 'แปลงเกษตรกรรม',
type: 'polygon',
zIndex: 1,
},
[
{
type: 'Feature',
id: 'plot-1',
properties: { owner: 'สมชาย', crop: 'ข้าวหอมมะลิ', ndvi: 0.72 },
geometry: {
type: 'Polygon',
coordinates: [
[
[100.50, 13.75],
[100.51, 13.75],
[100.51, 13.76],
[100.50, 13.76],
[100.50, 13.75],
],
],
},
},
]
);
// 4. สไตล์ Thematic แบบ Categorical ตามประเภทพืช
ofs.legendTools.setThematicCategorical('parcels', 'crop', 'category10');
// 5. ติดตั้ง Legend UI Widget ลงใน DOM (ถ้ามี container)
const legendMount = document.getElementById('legend-container');
if (legendMount) {
const legendWidget = new LegendWidget(ofs.legendTools);
legendWidget.mount(legendMount);
}
// 6. ติดตั้ง AutoCAD Web CLI Console Widget
const consoleMount = document.getElementById('console-container');
if (consoleMount) {
ofs.consoleWidget.mount(consoleMount);
}
// 7. สั่งเปิดโหมดวาดรูปแปลงที่ดิน
ofs.drawTools.setMode('polygon', 'parcels');
}
initMap();ความสามารถโดยย่อ
| เสาหลัก | ทำอะไร |
|---|---|
| Data Adapter (ofs.dataSource) | เก็บเลเยอร์/ฟีเจอร์ GeoJSON, R-tree index, Undo/Redo แบบ transaction, event 20 ชนิด |
| Draw Tools & CAD (ofs.drawTools) | วาด point/line/polygon/circle/rectangle, OSnap, ORTHO/POLAR/OTRACK, grips, move/copy/rotate/scale/trim/extend/fillet/offset/array |
| Legend & Symbology (ofs.legendTools, ofs.thematicEngine) | สัญลักษณ์แบบ QGIS: single / categorized / graduated, ลายแรเงา, marker SDF, ชุดสี ColorBrewer/CARTO/viridis |
| Import / Export (ofs.io) | GeoJSON, Shapefile (ภาษาไทย), GeoPackage, KML/KMZ, CSV, DXF พร้อมระบบพิกัดและ datum shift |
| Topology (ofs.topology) | ตรวจรูปทรงผิด, ส่วนซ้อน, ช่องว่าง, sliver, จุดเกือบชน และแก้แบบผู้ใช้ยืนยัน (รองรับเลเยอร์หลักแสนแปลง) |
| Attributes (ofs.attributes) | schema ฟิลด์, ตาราง, เครื่องคิดเลขฟิลด์, select by attribute / location, สถิติ |
| Layout (grid, network) | วางจุดเป็นตารางในขอบเขต และต่อจุดเป็นโครงข่ายจากต้นทาง คำนวณเป็นเมตรบนระนาบจริง |
| Map Adapters (ofs.mapAdapter) | MapLibre GL v6 / Mapbox GL v3 ผ่าน interface เดียว หรือ attach() แผนที่ที่แอปสร้างเอง |
| Web CLI & AI (ofs.commandEngine, ofs.aiBridge) | คอมมานด์ไลน์แบบ AutoCAD 67 คำสั่ง และ Function Calling ให้ AI เรียกคำสั่งเดียวกับที่คนพิมพ์ |
รายละเอียดของแต่ละส่วนพร้อมตัวอย่างโค้ดอยู่ใน docs/API.md
(ติดตั้งแล้วจะอยู่ที่ node_modules/ofs-object/docs/API.md)
คำสั่งในตัว (Built-in Commands)
| คำสั่ง (Command) | คำสั่งย่อ (Alias) | พารามิเตอร์ | คำอธิบาย |
|:---|:---|:---|:---|
| line / draw_line | l | [coordinates] | วาดเส้นตรง (คลิกบนแผนที่ หรือระบุพิกัด JSON) |
| polygon / draw_polygon| poly, p | [coordinates] | วาดรูปแปลง Polygon |
| point / draw_point | pt | [lng, lat] | วาดจุดพิกัด Point |
| circle | - | [radius] | วาดวงกลม (คลิกจุดศูนย์กลาง + รัศมี) |
| rectangle | rect | - | วาดรูปสี่เหลี่ยมผืนผ้า |
| guides | guide | [on\|off] [--crosshair on\|off] [--dimensions on\|off] [--segments on\|off] [--area on\|off] [--angles on\|off] [--anchor cursor\|vertex] | เส้น guide พาดจอ, เส้นบอกระยะแบบแปลน และมุมที่หัวมุม ขณะวาด |
| ortho | orthomode | [on\|off] | เปิด/ปิดการล็อกแกน 90 องศา (F8) |
| osnap | snap | [on\|off\|end\|int\|mid\|cen\|per\|nea] | ตั้งค่า OSnap: จุดยอด, จุดตัด, กึ่งกลาง, ศูนย์กลางวงกลม, ตั้งฉาก, บนเส้น (F3 เปิด/ปิด) |
| polar | polartracking | [on\|off\|<มุม>] | Polar Tracking ล็อกทิศตามมุมที่กำหนด เช่น polar 15 (F10) |
| ucs | - | [world\|<มุมแกน X>] | หมุนแกนวาดให้ตามแนวแปลง (คลิก 2 จุดตามขอบแปลง) ใช้กับ ORTHO, POLAR และ @dx,dy |
| angles | angdir, units | [bearing\|cad] | รูปแบบมุม: bearing (0°=เหนือ ตามเข็ม) หรือ cad (0°=ตะวันออก ทวนเข็ม) |
| offset | o | [distance_meters [--copy\|-c]] | ไม่ใส่ค่า: พิมพ์ระยะ (หรือ T = ผ่านจุด) แล้วคลิกวัตถุและคลิกฝั่ง สร้างวัตถุใหม่ได้ต่อเนื่อง / ใส่ค่า: ขนานวัตถุที่เลือก (+ ขวา/ขยายออก, - ซ้าย/ยุบเข้า) |
| rotate | ro | [angle] | หมุนวัตถุที่เลือกรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์มุม / คลิกทิศ / R (Reference มุมเดิม→มุมใหม่) |
| scale | sc | [factor] | ย่อ/ขยายรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์ factor หรือ R (ความยาวอ้างอิง→ความยาวใหม่) |
| mirror | mi | - | สะท้อนวัตถุที่เลือกข้ามเส้นที่คลิก 2 จุด (เลือกได้ว่าลบต้นฉบับหรือไม่) |
| explode | - | - | แยกเส้นขอบของ Polygon ออกเป็นเส้นย่อย |
| fillet | f | [vertex_index radius] | ไม่ใส่ค่า: คลิก 2 เส้นเพื่อต่อมุม (รัศมี 0 = มุมแหลม, R ตั้งรัศมี = ส่วนโค้งวงกลมจริง) / ใส่ค่า: ลบมุมที่ vertex ของวัตถุที่เลือก |
| trim | tr | - | คลิกส่วนของเส้นที่ต้องการตัดทิ้ง ระหว่างจุดตัดกับวัตถุอื่น (Enter จบ) |
| extend | ex | - | คลิกใกล้ปลายเส้นเพื่อยืดไปชนวัตถุที่ใกล้ที่สุดตามแนวเส้น (Enter จบ) |
| stretch | s | - | ลากกรอบ แล้วย้ายเฉพาะจุดยอดที่อยู่ในกรอบ (ถ้ามี selection จะทำเฉพาะวัตถุที่เลือก) |
| arrayrect | array, ar | [{"rows","columns","rowSpacing","columnSpacing"}] | คัดลอกวัตถุที่เลือกเป็นตาราง แถว/คอลัมน์ตามแกน UCS ระยะเป็นเมตร |
| arraypolar | ap | [{"center","count","fillAngle","rotateItems"}] | คัดลอกวัตถุที่เลือกรอบจุดศูนย์กลาง |
| move | m, translate | [dist [angle]] | ย้ายวัตถุที่เลือก ไม่ใส่ค่า: เลือกจุดฐาน แล้วคลิกจุดที่สอง หรือพิมพ์ @dx,dy / @ระยะ<มุม |
| buffer | b, buf | <dist> [units] | สร้างพื้นที่กันชนรอบ Feature ที่เลือก |
| area | measure_area | - | คำนวณเนื้อที่ (ไร่ และ ตร.ม.) ของแปลงที่เลือก |
| union | merge | [snap_tolerance_meters] | รวมแปลงที่เลือกตั้งแต่ 2 แปลงขึ้นไป (รวมจุดยอดที่ห่างไม่เกิน 1 มม. หรือตามระยะที่ระบุ) |
| split | cut | [cutter_id] | ตัดแบ่ง Polygon ด้วยเส้นตัด (Auto-detect เส้นตัดข้ามเลเยอร์อัตโนมัติ) หรือตัดเส้น Line |
| split_overlap | slice | - | แยก 2 Polygon ที่ซ้อนทับกันออกเป็น 3 ชิ้นส่วน ($A \setminus B, A \cap B, B \setminus A$) |
| split_line | splitline | [cutter_id] | ตัดแบ่งเส้น LineString ออกเป็น 2 ท่อน ด้วยพิกัดจุดหรือเส้นตัด (เดิมมี alias break ซึ่งตอนนี้เป็นคำสั่ง BREAK แบบ AutoCAD) |
| break | br | - | คลิก 2 จุดบนเส้นเพื่อลบช่วงระหว่างจุด (คลิกจุดเดียวกัน 2 ครั้ง = แบ่งเส้นที่จุดนั้น) |
| lengthen | len | - | ปรับความยาวเส้นที่ปลายที่คลิก: DE (เพิ่ม/ลดเมตร), T (ความยาวรวม), P (เปอร์เซ็นต์) |
| align | al | - | ย้าย+หมุนวัตถุที่เลือกให้จุดต้นทาง 2 จุดตรงกับจุดปลายทาง (เลือกย่อ/ขยายให้พอดีได้) |
| otrack | tracking | [on\|off] | Object Snap Tracking (F11): ชี้ค้างที่จุด snap เพื่อเก็บจุด แล้วเคอร์เซอร์จะเกาะแนวแกน/มุม POLAR จากจุดนั้น |
| join | connect, j | [tolerance_meters] | เชื่อมต่อเส้น LineString 2 เส้นเข้าด้วยกัน (รักษา Direction ของเส้นแรกเป็นหลัก) |
| reverse | rev | - | กลับทิศทางของเส้น LineString (พร้อมแสดงลูกศรทิศทาง ▲ บนแผนที่ในโหมด Grips) |
| thematic | classify | <layer> <field> [method] [classes] [palette] | จัดสไตล์แผนที่เฉพาะเรื่อง |
| import | open, load | [url] [--crs EPSG:32647] [--encoding tis-620] [--close-rings] | นำเข้า GeoJSON / Shapefile / GeoPackage / KML / KMZ / CSV / DXF (ไม่ใส่ URL = เปิดหน้าต่างเลือกไฟล์) แล้วซูมไปที่ข้อมูล |
| export | save, download | [layer[,layer2]] <geojson\|shapefile\|gpkg\|kml\|kmz\|csv\|dxf> [--crs] [--encoding tis-620] [--name] [--xy] | ส่งออกเลเยอร์ (ค่าเริ่มต้นคือเลเยอร์ที่ active) |
| symbology | sym, style | [layer] single\|categorized <field>\|graduated <field> [--method jenks --classes 5 --ramp YlGn --mode size --sizes 2,12] | จัด symbology แบบ QGIS พร้อม symbol options: --color --opacity --outline --outline-width --outline-dash --width --dash --fill solid\|none\|lines\|cross\|dots --angle --spacing --hatch-width --background --shape --icon --size --stroke --rotation --anchor; class <n\|all\|other> on\|off, class <n> color\|label\|size, breaks 10,20,50, reclassify, ramps, shapes; รับ JSON ได้ |
| topology | topo, checkgeom | [check] [layer] [--checks …] [--near 0.1] [--area-tol 0.01] [--gap-max] [--sliver-width] [--min-area] [--show] · list [code] · zoom <n> · fix <n\|all\|code> [--from id\|smaller\|larger] [--into id\|longest\|largest] [--move id] [--yes] · show\|hide\|clear | ตรวจ topology ของเลเยอร์, ซูมไปที่ปัญหา, แก้ตามที่สั่ง (undo ได้); รับ JSON ได้ |
| fields | schema, field | [layer] · add <name> <type> [--default v] [--values a,b] [--expr "…"] · delete <name> · rename <old> <new> · set <name> [--alias …] | ดู/แก้ schema ของฟิลด์ (undo ได้) |
| table | attributes, attr | [layer] [--filter "expr"] [--fields a,b] [--sort f] [--desc] [--limit 20] [--selected] | แสดงตาราง attribute ใน console |
| calc | calculate, fieldcalc | <field> = <expression> [--layer L] [--selected] [--create] [--type number] [--skip-errors] · calc functions | เครื่องคิดเลขฟิลด์ เช่น calc ไร่ = round($area / 1600, 2) --create |
| select | query, selectattr | <expression> [--layer L] [--add\|--remove\|--intersect] | เลือกฟีเจอร์ด้วยเงื่อนไข attribute แล้วซูมไปที่ผลลัพธ์ |
| selectloc | select_location | <intersects\|within\|contains\|disjoint\|touches\|crosses\|within-distance> <reference layer> [--distance 50] | เลือกตามความสัมพันธ์เชิงตำแหน่งกับอีกเลเยอร์ |
| stats | statistics | <field> [--layer L] [--selected] | สถิติของฟิลด์ (count, sum, mean, median, sd, distinct) |
| grid | points_in_polygon | <boundary> --spacing 10 [--pattern square\|triangular] [--rotate 30] [--inset 2] [--into L] | เติมจุดเป็นตารางในขอบเขต ระยะเป็นเมตรบนระนาบจริง ตัดตามขอบ |
| network | connect | <points> --from 100.5,13.7 [--mode rows\|tree] [--angle 30] [--tolerance 1] [--into L] | ต่อจุดเป็นโครงข่ายจากต้นทาง คืนความยาวรายเส้น |
| geometry | geom, shape | [layer] [--id F] [--selected] [--limit 10] [--precision 6] [--bbox-only] | อ่านรูปทรงกลับมาเป็น GeoJSON พร้อมพื้นที่/ความยาว/bbox (หน่วยเมตร) |
| label | labels, annotate | [layer] <field> [--size 12] [--color] [--halo] [--offset 0,1] [--placement line] [--decimals 2] [--min-zoom 12] [--overlap] · [layer] --expr "…" · [layer] off\|show | ป้ายชื่อจากฟิลด์หรือนิพจน์ |
| measure | dist, mea | [distance\|area\|angle\|feature] [lng,lat …] | วัดระยะ/พื้นที่/มุม (คลิกบนแผนที่ หรือใส่พิกัด) พื้นที่แสดงเป็น ตร.ม. และ ไร่-งาน-วา |
| print | mapexport | [--width 1600] [--title "…"] [--name farm-map] [--jpeg] [--no-scale] [--no-north] [--attribution "…"] | บันทึกภาพแผนที่พร้อมมาตราส่วนและลูกศรทิศเหนือ |
| help | ?, commands | [command] | รายชื่อคำสั่ง หรือพารามิเตอร์ของคำสั่งเดียว (สร้างจาก schema) |
| macro | script | record <name> · stop · play <name> [--times 2] · list · show <name> · delete <name> · export · import <json> | บันทึกชุดคำสั่งแล้วเล่นซ้ำ (เก็บใน localStorage) |
| zoom | z, extent | [layerId] | ซูมไปยังขอบเขตของเลเยอร์หรือทั้งแผนที่ |
| undo / redo | u | - | ย้อนกลับ หรือทำซ้ำคำสั่งล่าสุด |
| help | ? | - | แสดงรายการคำสั่งทั้งหมดในระบบ |
ตัวอย่างการนำไปใช้ร่วมกับ Framework ต่างๆ
Vanilla JavaScript / TypeScript
<div id="map" style="width: 100vw; height: 100vh;"></div>
<div id="console-mount"></div>
<script type="module">
import { OFSObject } from 'ofs-object';
import * as maplibregl from 'maplibre-gl';
const ofs = new OFSObject({ mapOptions: { maplibregl } });
await ofs.init('map');
ofs.consoleWidget.mount('console-mount');
</script>
mount()ตกแต่งหน้าตาของ console อย่างเดียว ไม่จัดตำแหน่งให้ — วาง elementconsole-mountตาม layout ของหน้าเอง หรือใส่ classofs-console-floatingให้ลอยชิดล่างของ parent ที่position: relative(ก่อน 0.1.6 ไลบรารีบังคับposition: absolute; left: 80px; right: 420pxซึ่งเป็นขนาดของ playground)
React
import React, { useEffect, useRef } from 'react';
import { OFSObject, LegendWidget } from 'ofs-object';
import * as maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
export const MapView: React.FC = () => {
const mapContainerRef = useRef<HTMLDivElement>(null);
const legendRef = useRef<HTMLDivElement>(null);
const ofsRef = useRef<OFSObject | null>(null);
useEffect(() => {
if (!mapContainerRef.current) return;
const ofs = new OFSObject({ mapOptions: { maplibregl } });
ofsRef.current = ofs;
ofs.init(mapContainerRef.current, {
center: [100.5, 13.7],
zoom: 12,
}).then(() => {
if (legendRef.current) {
const legendWidget = new LegendWidget(ofs.legendTools);
legendWidget.mount(legendRef.current);
}
});
return () => {
ofs.destroy();
};
}, []);
return (
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
<div ref={legendRef} style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }} />
</div>
);
};Angular
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { OFSObject } from 'ofs-object';
@Component({
selector: 'app-map-view',
template: `
<div class="map-wrapper">
<div #mapContainer class="map-container"></div>
<div #consoleContainer class="console-container"></div>
</div>
`,
styles: [`
.map-wrapper { position: relative; width: 100vw; height: 100vh; }
.map-container { width: 100%; height: 100%; }
.console-container { position: absolute; bottom: 20px; left: 20px; width: 500px; z-index: 20; }
`]
})
export class MapViewComponent implements OnInit, OnDestroy {
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
@ViewChild('consoleContainer', { static: true }) consoleContainer!: ElementRef<HTMLDivElement>;
private ofs!: OFSObject;
async ngOnInit() {
this.ofs = new OFSObject({ mapOptions: { maplibregl } });
await this.ofs.init(this.mapContainer.nativeElement, {
center: [100.5, 13.7],
zoom: 13,
});
this.ofs.consoleWidget.mount(this.consoleContainer.nativeElement);
}
ngOnDestroy() {
this.ofs.destroy();
}
}Angular: กรณีที่ component สร้างแผนที่เอง
ใช้เมื่อโครงการมี map service / component ของตัวเองอยู่แล้ว หรือมีส่วนอื่นที่ต้องใช้ maplibregl.Map ตัวเดียวกัน — OFSObject จะเข้าไปใช้ร่วมโดยไม่ยึดความเป็นเจ้าของ:
import { Component, AfterViewInit, OnDestroy, ElementRef, ViewChild, NgZone } from '@angular/core';
import maplibregl from 'maplibre-gl';
import { OFSObject } from 'ofs-object';
@Component({
selector: 'app-map-view',
template: `
<div #mapContainer class="map-container"></div>
<div #consoleContainer class="console-container"></div>
`,
})
export class MapViewComponent implements AfterViewInit, OnDestroy {
@ViewChild('mapContainer') mapContainer!: ElementRef<HTMLDivElement>;
@ViewChild('consoleContainer') consoleContainer!: ElementRef<HTMLDivElement>;
private map!: maplibregl.Map;
private ofs!: OFSObject;
constructor(private readonly zone: NgZone) {}
async ngAfterViewInit() {
// แผนที่เป็นของ component นี้ จะตั้งค่า control / source อื่นเพิ่มเองก็ได้
this.map = new maplibregl.Map({
container: this.mapContainer.nativeElement,
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
center: [100.5, 13.7],
zoom: 13,
});
this.map.addControl(new maplibregl.NavigationControl());
// เมาส์ทุก event ของแผนที่ไม่ควรไปปลุก change detection ของ Angular
await this.zone.runOutsideAngular(async () => {
this.ofs = new OFSObject({ map: this.map });
await this.ofs.init();
});
this.ofs.consoleWidget.mount(this.consoleContainer.nativeElement);
}
ngOnDestroy() {
this.ofs.destroy(); // ถอดเฉพาะของ OFSObject ออก แผนที่ยังอยู่
this.map.remove(); // แผนที่เป็นของเรา จึงลบเอง
}
}ลำดับสำคัญ: เรียก
ofs.destroy()ก่อนmap.remove()เสมอ เพื่อให้ถอด source/layer ออกจากแผนที่ที่ยังใช้งานได้อยู่
Angular: ใช้ Command Engine กับ UI ที่ทำเอง (ไม่ใช้ CSS ของไลบรารี)
consoleWidget ทำงานได้โดยไม่ต้อง mount() — สั่งงานด้วย submit() แล้ววาดผลจาก event เอง ไม่ต้องใช้ ofs-widgets.css เลย:
export class CommandBarComponent implements OnInit, OnDestroy {
lines: ConsoleLine[] = [];
prompt: string | null = null; // ไม่ใช่ null = คำสั่งกำลังรอคำตอบ
private off: (() => void)[] = [];
ngOnInit() {
const c = this.ofs.consoleWidget!;
this.lines = [...c.getLines()]; // ข้อความที่เกิดก่อน component นี้ถูกสร้าง
this.off.push(
c.on('console:line', (line) => this.zone.run(() => this.lines = [...this.lines, line])),
c.on('console:cleared', () => this.zone.run(() => this.lines = [])),
c.on('console:prompt', ({ promptText }) => this.zone.run(() => this.prompt = promptText)),
);
}
send(text: string) {
this.ofs.consoleWidget!.submit(text); // ตอบ prompt ที่ค้างอยู่ หรือรันคำสั่งใหม่
}
ngOnDestroy() { this.off.forEach((fn) => fn()); }
}submit() ฉลาดพอที่จะรู้เองว่าข้อความนั้นคือคำตอบของ prompt ที่ค้างอยู่ หรือเป็นคำสั่งใหม่ และการคลิกแผนที่ตอบ requestPoint ก็ยังทำงานตามปกติเพราะ OFSObject ต่อ drawTools เป็น point provider ให้แล้ว
หรือเปลี่ยน console ทั้งตัวเป็นของคุณเอง โดย implement IOFSConsole (7 method) แล้วส่งเข้าไป:
const ofs = new OFSObject({ map, console: myAngularConsole });
// ofs.console คือตัวที่คำสั่งใช้ · ofs.consoleWidget เป็น nullวิธีหลังนี้ enableDynamicInput() จะใช้ไม่ได้ (HUD ผูกกับ widget เดิม) ส่วนใหญ่จึงควรใช้วิธีแรก
React ใช้รูปแบบเดียวกันใน
useEffect(สร้าง map ใน ref แล้วnew OFSObject({ map })) ส่วน Vue 3 ใช้ในonMounted/onUnmounted
Vue 3
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { OFSObject, LegendWidget } from 'ofs-object';
import * as maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
const mapContainer = ref<HTMLDivElement | null>(null);
const legendContainer = ref<HTMLDivElement | null>(null);
let ofs: OFSObject | null = null;
onMounted(async () => {
if (!mapContainer.value) return;
ofs = new OFSObject({ mapOptions: { maplibregl } });
await ofs.init(mapContainer.value, {
center: [100.5, 13.7],
zoom: 12,
});
if (legendContainer.value) {
const legendWidget = new LegendWidget(ofs.legendTools);
legendWidget.mount(legendContainer.value);
}
});
onUnmounted(() => {
ofs?.destroy();
});
</script>
<template>
<div class="map-layout">
<div ref="mapContainer" class="map-container"></div>
<div ref="legendContainer" class="legend-panel"></div>
</div>
</template>
<style scoped>
.map-layout { position: relative; width: 100%; height: 100vh; }
.map-container { width: 100%; height: 100%; }
.legend-panel { position: absolute; top: 20px; right: 20px; z-index: 10; }
</style>คู่มือฉบับเต็ม
ความสามารถทั้งหมดพร้อมตัวอย่างโค้ดทีละส่วนอยู่ใน docs/API.md ซึ่งมาพร้อมแพ็กเกจ
เปิดได้จาก node_modules/ofs-object/docs/API.md
สัญญาอนุญาต (License)
MIT License © On-Farm Solution Co.,Ltd.
