ofs-object 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 On-Farm Solution Co.,Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,511 @@
1
+ # OFSObject (Map Tools) 🗺️📐
2
+
3
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-blue.svg)](https://www.typescriptlang.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Tests](https://img.shields.io/badge/Tests-76%20Passed-brightgreen.svg)]()
6
+
7
+ **OFSObject (`ofs-object`)** คือไลบรารี TypeScript แบบ **Framework-Agnostic** สำหรับพัฒนาระบบแผนที่และเครื่องมือ GIS/CAD ขั้นสูง ออกแบบมาให้ทำงานร่วมกับได้ทุกเฟรมเวิร์ก (Angular, React, Vue, Svelte หรือ Vanilla JavaScript) โดยไม่มีข้อผูกมัดกับเฟรมเวิร์กใดเฟรมเวิร์กหนึ่ง
8
+
9
+ ไลบรารีนี้รวม 5 เสาหลักสำหรับงานแผนที่สมัยใหม่:
10
+ 1. **Data Adapter (`OFSDataSource`)**: ศูนย์กลางจัดการข้อมูลแผนที่ (Single Source of Truth) รองรับ Layer, FeatureCollection, Spatial Query, Turf.js Analytics และระบบ Undo/Redo Transaction
11
+ 2. **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)
12
+ 3. **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
13
+ 4. **Map Adapter (`IMapAdapter`)**: สถาปัตยกรรม Abstraction กลางรองรับทั้ง **MapLibre GL JS** และ **Mapbox GL JS** สลับ Map Engine ได้อย่างราบรื่น
14
+ 5. **Web CLI & AI Bridge (`OFSCommandEngine`, `OFSWebConsoleWidget`, `OFSAiBridge`)**: ระบบ Pipeline สั่งการแบบ AutoCAD Command Line (รองรับคำสั่งหลายขั้นตอนและการรอรับ Input จากแผนที่) พร้อม DOM Console UI และ AI Bridge แปลงคำสั่งธรรมชาติของ LLM (Gemini, GPT) เป็น Tool Calling พร้อมส่ง Feedback Loop กลับ
15
+
16
+ ---
17
+ ## สารบัญ (Table of Contents)
18
+
19
+ 1. [การติดตั้ง](#การติดตั้ง-installation)
20
+ 2. [เริ่มต้นใช้งานด่วน](#เริ่มต้นใช้งานด่วน-quick-start)
21
+ 3. [ความสามารถโดยย่อ](#ความสามารถโดยย่อ)
22
+ 4. [คำสั่งในตัว](#คำสั่งในตัว-built-in-commands)
23
+ 5. [ตัวอย่างการใช้กับ Framework](#ตัวอย่างการนำไปใช้ร่วมกับ-framework-ต่างๆ)
24
+ 6. [คู่มือฉบับเต็ม](#คู่มือฉบับเต็ม)
25
+
26
+ ---
27
+
28
+ ## การติดตั้ง (Installation)
29
+
30
+ > **สถานะ: beta** — เผยแพร่ภายใต้ tag `next` ยังไม่ผ่านการทดสอบด้วยการใช้งานจริงในเบราว์เซอร์ครบทุกส่วน
31
+ > (ดูหัวข้อ "สิ่งที่ต้องทดสอบใน Browser" ใน `ROADMAP.md`) API อาจเปลี่ยนได้ในรุ่นถัดไป
32
+
33
+ ```bash
34
+ npm install ofs-object@next
35
+ ```
36
+
37
+ ### Peer Dependencies
38
+ เนื่องจากไลบรารีออกแบบให้ Framework-Agnostic จึงเปิดให้เลือกใช้ Map Engine ที่เหมาะสมกับโปรเจกต์ของคุณ:
39
+
40
+ ```bash
41
+ # สำหรับ MapLibre GL JS (ฟรี, Open-source แนะนำ) — รองรับ v6 ขึ้นไป
42
+ npm install maplibre-gl
43
+
44
+ # หรือสำหรับ Mapbox GL JS (Commercial) — รองรับ v3 ขึ้นไป
45
+ npm install mapbox-gl
46
+ ```
47
+
48
+ > MapLibre v6 เป็น ES module ล้วน (ไม่มี CommonJS) ถ้าจะใช้ `require()` ใน Node ต้องใช้ MapLibre v5 หรือใช้ Mapbox แทน — ผ่าน bundler (Angular, Vite, webpack) ใช้ได้ตามปกติ
49
+
50
+ ### Stylesheet
51
+
52
+ Widget ที่ไลบรารี mount ให้ (Command Console, Legend, แผง Symbology, ตาราง Attribute, แผง Topology) ต้องการไฟล์ CSS หนึ่งไฟล์:
53
+
54
+ ```css
55
+ @import 'maplibre-gl/dist/maplibre-gl.css';
56
+ @import 'ofs-object/styles.css';
57
+ ```
58
+
59
+ Angular ใส่ใน `angular.json` → `styles`:
60
+
61
+ ```json
62
+ "styles": [
63
+ "node_modules/maplibre-gl/dist/maplibre-gl.css",
64
+ "node_modules/ofs-object/dist/ofs-object.css",
65
+ "src/styles.css"
66
+ ]
67
+ ```
68
+
69
+ ไฟล์นี้**ไม่มี** CSS reset, ไม่ตั้งค่า `body` และไม่ดึงเว็บฟอนต์ — มีแต่สไตล์ของ widget เท่านั้น สีทั้งหมดเป็น custom property ที่เปลี่ยนทับได้:
70
+
71
+ ```css
72
+ :root {
73
+ --ofs-accent-blue: #2563eb;
74
+ --ofs-text-primary: #111827;
75
+ }
76
+ ```
77
+
78
+ ### ติดตั้งจาก Git (สำหรับรุ่นที่ยังไม่ publish)
79
+
80
+ ```bash
81
+ npm install git+ssh://git@github.com/<user>/OnFarmObject.git#main
82
+ ```
83
+
84
+ `prepare` script จะ build `dist/` ให้อัตโนมัติตอนติดตั้ง (npm ลง devDependencies ชั่วคราวเพื่อ build แล้วตัดทิ้ง) — ต้องใช้ `--force` เมื่ออัปเดต เพราะ npm cache แยกตาม commit
85
+
86
+ > แพ็กเกจที่เผยแพร่มีเฉพาะ `dist/` (bundle + `index.d.ts` + `ofs-object.css`) ไม่มีซอร์ส TypeScript
87
+
88
+ ---
89
+
90
+ ---
91
+
92
+ ## เริ่มต้นใช้งานด่วน (Quick Start)
93
+
94
+ สร้างระบบแผนที่พร้อมเครื่องมือครบวงจรได้ในไม่กี่บรรทัดผ่านคลาส `OFSObject`:
95
+
96
+ ```typescript
97
+ import { OFSObject, MapLibreAdapter, LegendWidget } from 'ofs-object';
98
+ import 'maplibre-gl/dist/maplibre-gl.css';
99
+
100
+ async function initMap() {
101
+ // 1. สร้าง Instance หลักของ OFSObject (ใช้ MapLibre เป็นค่าเริ่มต้น)
102
+ const ofs = new OFSObject({
103
+ mapAdapter: new MapLibreAdapter(),
104
+ });
105
+
106
+ // 2. กำหนดแผนที่ลงบน HTML Container Element id="map"
107
+ await ofs.init('map', {
108
+ center: [100.5018, 13.7563], // กรุงเทพฯ [lng, lat]
109
+ zoom: 12,
110
+ style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
111
+ });
112
+
113
+ // 3. เพิ่มเลเยอร์ข้อมูลแปลงที่ดิน (Polygon)
114
+ const layer = ofs.dataSource.addLayer(
115
+ {
116
+ id: 'parcels',
117
+ name: 'แปลงเกษตรกรรม',
118
+ type: 'polygon',
119
+ zIndex: 1,
120
+ },
121
+ [
122
+ {
123
+ type: 'Feature',
124
+ id: 'plot-1',
125
+ properties: { owner: 'สมชาย', crop: 'ข้าวหอมมะลิ', ndvi: 0.72 },
126
+ geometry: {
127
+ type: 'Polygon',
128
+ coordinates: [
129
+ [
130
+ [100.50, 13.75],
131
+ [100.51, 13.75],
132
+ [100.51, 13.76],
133
+ [100.50, 13.76],
134
+ [100.50, 13.75],
135
+ ],
136
+ ],
137
+ },
138
+ },
139
+ ]
140
+ );
141
+
142
+ // 4. สไตล์ Thematic แบบ Categorical ตามประเภทพืช
143
+ ofs.legendTools.setThematicCategorical('parcels', 'crop', 'category10');
144
+
145
+ // 5. ติดตั้ง Legend UI Widget ลงใน DOM (ถ้ามี container)
146
+ const legendMount = document.getElementById('legend-container');
147
+ if (legendMount) {
148
+ const legendWidget = new LegendWidget(ofs.legendTools);
149
+ legendWidget.mount(legendMount);
150
+ }
151
+
152
+ // 6. ติดตั้ง AutoCAD Web CLI Console Widget
153
+ const consoleMount = document.getElementById('console-container');
154
+ if (consoleMount) {
155
+ ofs.consoleWidget.mount(consoleMount);
156
+ }
157
+
158
+ // 7. สั่งเปิดโหมดวาดรูปแปลงที่ดิน
159
+ ofs.drawTools.setMode('polygon', 'parcels');
160
+ }
161
+
162
+ initMap();
163
+ ```
164
+
165
+ ---
166
+
167
+ ---
168
+
169
+ ## ความสามารถโดยย่อ
170
+
171
+ | เสาหลัก | ทำอะไร |
172
+ |---|---|
173
+ | **Data Adapter** (`ofs.dataSource`) | เก็บเลเยอร์/ฟีเจอร์ GeoJSON, R-tree index, Undo/Redo แบบ transaction, event 20 ชนิด |
174
+ | **Draw Tools & CAD** (`ofs.drawTools`) | วาด point/line/polygon/circle/rectangle, OSnap, ORTHO/POLAR/OTRACK, grips, move/copy/rotate/scale/trim/extend/fillet/offset/array |
175
+ | **Legend & Symbology** (`ofs.legendTools`, `ofs.thematicEngine`) | สัญลักษณ์แบบ QGIS: single / categorized / graduated, ลายแรเงา, marker SDF, ชุดสี ColorBrewer/CARTO/viridis |
176
+ | **Import / Export** (`ofs.io`) | GeoJSON, Shapefile (ภาษาไทย), GeoPackage, KML/KMZ, CSV, DXF พร้อมระบบพิกัดและ datum shift |
177
+ | **Topology** (`ofs.topology`) | ตรวจรูปทรงผิด, ส่วนซ้อน, ช่องว่าง, sliver, จุดเกือบชน และแก้แบบผู้ใช้ยืนยัน (รองรับเลเยอร์หลักแสนแปลง) |
178
+ | **Attributes** (`ofs.attributes`) | schema ฟิลด์, ตาราง, เครื่องคิดเลขฟิลด์, select by attribute / location, สถิติ |
179
+ | **Layout** (`grid`, `network`) | วางจุดเป็นตารางในขอบเขต และต่อจุดเป็นโครงข่ายจากต้นทาง คำนวณเป็นเมตรบนระนาบจริง |
180
+ | **Map Adapters** (`ofs.mapAdapter`) | MapLibre GL v6 / Mapbox GL v3 ผ่าน interface เดียว หรือ `attach()` แผนที่ที่แอปสร้างเอง |
181
+ | **Web CLI & AI** (`ofs.commandEngine`, `ofs.aiBridge`) | คอมมานด์ไลน์แบบ AutoCAD 67 คำสั่ง และ Function Calling ให้ AI เรียกคำสั่งเดียวกับที่คนพิมพ์ |
182
+
183
+ รายละเอียดของแต่ละส่วนพร้อมตัวอย่างโค้ดอยู่ใน **[`docs/API.md`](docs/API.md)**
184
+ (ติดตั้งแล้วจะอยู่ที่ `node_modules/ofs-object/docs/API.md`)
185
+
186
+ ---
187
+
188
+ ## คำสั่งในตัว (Built-in Commands)
189
+
190
+ | คำสั่ง (Command) | คำสั่งย่อ (Alias) | พารามิเตอร์ | คำอธิบาย |
191
+ |:---|:---|:---|:---|
192
+ | `line` / `draw_line` | `l` | `[coordinates]` | วาดเส้นตรง (คลิกบนแผนที่ หรือระบุพิกัด JSON) |
193
+ | `polygon` / `draw_polygon`| `poly`, `p` | `[coordinates]` | วาดรูปแปลง Polygon |
194
+ | `point` / `draw_point` | `pt` | `[lng, lat]` | วาดจุดพิกัด Point |
195
+ | `circle` | - | `[radius]` | วาดวงกลม (คลิกจุดศูนย์กลาง + รัศมี) |
196
+ | `rectangle` | `rect` | - | วาดรูปสี่เหลี่ยมผืนผ้า |
197
+ | `guides` | `guide` | `[on\|off] [--crosshair on\|off] [--dimensions on\|off] [--segments on\|off] [--area on\|off] [--angles on\|off] [--anchor cursor\|vertex]` | เส้น guide พาดจอ, เส้นบอกระยะแบบแปลน และมุมที่หัวมุม ขณะวาด |
198
+ | `ortho` | `orthomode` | `[on\|off]` | เปิด/ปิดการล็อกแกน 90 องศา (F8) |
199
+ | `osnap` | `snap` | `[on\|off\|end\|int\|mid\|cen\|per\|nea]` | ตั้งค่า OSnap: จุดยอด, จุดตัด, กึ่งกลาง, ศูนย์กลางวงกลม, ตั้งฉาก, บนเส้น (F3 เปิด/ปิด) |
200
+ | `polar` | `polartracking` | `[on\|off\|<มุม>]` | Polar Tracking ล็อกทิศตามมุมที่กำหนด เช่น `polar 15` (F10) |
201
+ | `ucs` | - | `[world\|<มุมแกน X>]` | หมุนแกนวาดให้ตามแนวแปลง (คลิก 2 จุดตามขอบแปลง) ใช้กับ ORTHO, POLAR และ `@dx,dy` |
202
+ | `angles` | `angdir`, `units` | `[bearing\|cad]` | รูปแบบมุม: bearing (0°=เหนือ ตามเข็ม) หรือ cad (0°=ตะวันออก ทวนเข็ม) |
203
+ | `offset` | `o` | `[distance_meters [--copy\|-c]]` | ไม่ใส่ค่า: พิมพ์ระยะ (หรือ `T` = ผ่านจุด) แล้วคลิกวัตถุและคลิกฝั่ง สร้างวัตถุใหม่ได้ต่อเนื่อง / ใส่ค่า: ขนานวัตถุที่เลือก (+ ขวา/ขยายออก, - ซ้าย/ยุบเข้า) |
204
+ | `rotate` | `ro` | `[angle]` | หมุนวัตถุที่เลือกรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์มุม / คลิกทิศ / `R` (Reference มุมเดิม→มุมใหม่) |
205
+ | `scale` | `sc` | `[factor]` | ย่อ/ขยายรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์ factor หรือ `R` (ความยาวอ้างอิง→ความยาวใหม่) |
206
+ | `mirror` | `mi` | - | สะท้อนวัตถุที่เลือกข้ามเส้นที่คลิก 2 จุด (เลือกได้ว่าลบต้นฉบับหรือไม่) |
207
+ | `explode` | - | - | แยกเส้นขอบของ Polygon ออกเป็นเส้นย่อย |
208
+ | `fillet` | `f` | `[vertex_index radius]` | ไม่ใส่ค่า: คลิก 2 เส้นเพื่อต่อมุม (รัศมี 0 = มุมแหลม, `R` ตั้งรัศมี = ส่วนโค้งวงกลมจริง) / ใส่ค่า: ลบมุมที่ vertex ของวัตถุที่เลือก |
209
+ | `trim` | `tr` | - | คลิกส่วนของเส้นที่ต้องการตัดทิ้ง ระหว่างจุดตัดกับวัตถุอื่น (Enter จบ) |
210
+ | `extend` | `ex` | - | คลิกใกล้ปลายเส้นเพื่อยืดไปชนวัตถุที่ใกล้ที่สุดตามแนวเส้น (Enter จบ) |
211
+ | `stretch` | `s` | - | ลากกรอบ แล้วย้ายเฉพาะจุดยอดที่อยู่ในกรอบ (ถ้ามี selection จะทำเฉพาะวัตถุที่เลือก) |
212
+ | `arrayrect` | `array`, `ar` | `[{"rows","columns","rowSpacing","columnSpacing"}]` | คัดลอกวัตถุที่เลือกเป็นตาราง แถว/คอลัมน์ตามแกน UCS ระยะเป็นเมตร |
213
+ | `arraypolar` | `ap` | `[{"center","count","fillAngle","rotateItems"}]` | คัดลอกวัตถุที่เลือกรอบจุดศูนย์กลาง |
214
+ | `move` | `m`, `translate` | `[dist [angle]]` | ย้ายวัตถุที่เลือก ไม่ใส่ค่า: เลือกจุดฐาน แล้วคลิกจุดที่สอง หรือพิมพ์ `@dx,dy` / `@ระยะ<มุม` |
215
+ | `buffer` | `b`, `buf` | `<dist> [units]` | สร้างพื้นที่กันชนรอบ Feature ที่เลือก |
216
+ | `area` | `measure_area` | - | คำนวณเนื้อที่ (ไร่ และ ตร.ม.) ของแปลงที่เลือก |
217
+ | `union` | `merge` | `[snap_tolerance_meters]` | รวมแปลงที่เลือกตั้งแต่ 2 แปลงขึ้นไป (รวมจุดยอดที่ห่างไม่เกิน 1 มม. หรือตามระยะที่ระบุ) |
218
+ | `split` | `cut` | `[cutter_id]` | ตัดแบ่ง Polygon ด้วยเส้นตัด (Auto-detect เส้นตัดข้ามเลเยอร์อัตโนมัติ) หรือตัดเส้น Line |
219
+ | `split_overlap` | `slice` | - | แยก 2 Polygon ที่ซ้อนทับกันออกเป็น 3 ชิ้นส่วน ($A \setminus B, A \cap B, B \setminus A$) |
220
+ | `split_line` | `splitline` | `[cutter_id]` | ตัดแบ่งเส้น LineString ออกเป็น 2 ท่อน ด้วยพิกัดจุดหรือเส้นตัด (เดิมมี alias `break` ซึ่งตอนนี้เป็นคำสั่ง BREAK แบบ AutoCAD) |
221
+ | `break` | `br` | - | คลิก 2 จุดบนเส้นเพื่อลบช่วงระหว่างจุด (คลิกจุดเดียวกัน 2 ครั้ง = แบ่งเส้นที่จุดนั้น) |
222
+ | `lengthen` | `len` | - | ปรับความยาวเส้นที่ปลายที่คลิก: DE (เพิ่ม/ลดเมตร), T (ความยาวรวม), P (เปอร์เซ็นต์) |
223
+ | `align` | `al` | - | ย้าย+หมุนวัตถุที่เลือกให้จุดต้นทาง 2 จุดตรงกับจุดปลายทาง (เลือกย่อ/ขยายให้พอดีได้) |
224
+ | `otrack` | `tracking` | `[on\|off]` | Object Snap Tracking (F11): ชี้ค้างที่จุด snap เพื่อเก็บจุด แล้วเคอร์เซอร์จะเกาะแนวแกน/มุม POLAR จากจุดนั้น |
225
+ | `join` | `connect`, `j` | `[tolerance_meters]` | เชื่อมต่อเส้น LineString 2 เส้นเข้าด้วยกัน (รักษา Direction ของเส้นแรกเป็นหลัก) |
226
+ | `reverse` | `rev` | - | กลับทิศทางของเส้น LineString (พร้อมแสดงลูกศรทิศทาง `▲` บนแผนที่ในโหมด Grips) |
227
+ | `thematic` | `classify` | `<layer> <field> [method] [classes] [palette]` | จัดสไตล์แผนที่เฉพาะเรื่อง |
228
+ | `import` | `open`, `load` | `[url] [--crs EPSG:32647] [--encoding tis-620] [--close-rings]` | นำเข้า GeoJSON / Shapefile / GeoPackage / KML / KMZ / CSV / DXF (ไม่ใส่ URL = เปิดหน้าต่างเลือกไฟล์) แล้วซูมไปที่ข้อมูล |
229
+ | `export` | `save`, `download` | `[layer[,layer2]] <geojson\|shapefile\|gpkg\|kml\|kmz\|csv\|dxf> [--crs] [--encoding tis-620] [--name] [--xy]` | ส่งออกเลเยอร์ (ค่าเริ่มต้นคือเลเยอร์ที่ active) |
230
+ | `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 ได้ |
231
+ | `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 ได้ |
232
+ | `fields` | `schema`, `field` | `[layer]` · `add <name> <type> [--default v] [--values a,b] [--expr "…"]` · `delete <name>` · `rename <old> <new>` · `set <name> [--alias …]` | ดู/แก้ schema ของฟิลด์ (undo ได้) |
233
+ | `table` | `attributes`, `attr` | `[layer] [--filter "expr"] [--fields a,b] [--sort f] [--desc] [--limit 20] [--selected]` | แสดงตาราง attribute ใน console |
234
+ | `calc` | `calculate`, `fieldcalc` | `<field> = <expression> [--layer L] [--selected] [--create] [--type number] [--skip-errors]` · `calc functions` | เครื่องคิดเลขฟิลด์ เช่น `calc ไร่ = round($area / 1600, 2) --create` |
235
+ | `select` | `query`, `selectattr` | `<expression> [--layer L] [--add\|--remove\|--intersect]` | เลือกฟีเจอร์ด้วยเงื่อนไข attribute แล้วซูมไปที่ผลลัพธ์ |
236
+ | `selectloc` | `select_location` | `<intersects\|within\|contains\|disjoint\|touches\|crosses\|within-distance> <reference layer> [--distance 50]` | เลือกตามความสัมพันธ์เชิงตำแหน่งกับอีกเลเยอร์ |
237
+ | `stats` | `statistics` | `<field> [--layer L] [--selected]` | สถิติของฟิลด์ (count, sum, mean, median, sd, distinct) |
238
+ | `grid` | `points_in_polygon` | `<boundary> --spacing 10 [--pattern square\|triangular] [--rotate 30] [--inset 2] [--into L]` | เติมจุดเป็นตารางในขอบเขต ระยะเป็นเมตรบนระนาบจริง ตัดตามขอบ |
239
+ | `network` | `connect` | `<points> --from 100.5,13.7 [--mode rows\|tree] [--angle 30] [--tolerance 1] [--into L]` | ต่อจุดเป็นโครงข่ายจากต้นทาง คืนความยาวรายเส้น |
240
+ | `geometry` | `geom`, `shape` | `[layer] [--id F] [--selected] [--limit 10] [--precision 6] [--bbox-only]` | อ่านรูปทรงกลับมาเป็น GeoJSON พร้อมพื้นที่/ความยาว/bbox (หน่วยเมตร) |
241
+ | `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` | ป้ายชื่อจากฟิลด์หรือนิพจน์ |
242
+ | `measure` | `dist`, `mea` | `[distance\|area\|angle\|feature] [lng,lat …]` | วัดระยะ/พื้นที่/มุม (คลิกบนแผนที่ หรือใส่พิกัด) พื้นที่แสดงเป็น ตร.ม. และ ไร่-งาน-วา |
243
+ | `print` | `mapexport` | `[--width 1600] [--title "…"] [--name farm-map] [--jpeg] [--no-scale] [--no-north] [--attribution "…"]` | บันทึกภาพแผนที่พร้อมมาตราส่วนและลูกศรทิศเหนือ |
244
+ | `help` | `?`, `commands` | `[command]` | รายชื่อคำสั่ง หรือพารามิเตอร์ของคำสั่งเดียว (สร้างจาก schema) |
245
+ | `macro` | `script` | `record <name>` · `stop` · `play <name> [--times 2]` · `list` · `show <name>` · `delete <name>` · `export` · `import <json>` | บันทึกชุดคำสั่งแล้วเล่นซ้ำ (เก็บใน localStorage) |
246
+ | `zoom` | `z`, `extent` | `[layerId]` | ซูมไปยังขอบเขตของเลเยอร์หรือทั้งแผนที่ |
247
+ | `undo` / `redo` | `u` | - | ย้อนกลับ หรือทำซ้ำคำสั่งล่าสุด |
248
+ | `help` | `?` | - | แสดงรายการคำสั่งทั้งหมดในระบบ |
249
+
250
+ ---
251
+
252
+ ## ตัวอย่างการนำไปใช้ร่วมกับ Framework ต่างๆ
253
+
254
+ ### Vanilla JavaScript / TypeScript
255
+
256
+ ```html
257
+ <div id="map" style="width: 100vw; height: 100vh;"></div>
258
+ <div id="console-mount"></div>
259
+
260
+ <script type="module">
261
+ import { OFSObject } from 'ofs-object';
262
+
263
+ const ofs = new OFSObject();
264
+ await ofs.init('map');
265
+ ofs.consoleWidget.mount('console-mount');
266
+ </script>
267
+ ```
268
+
269
+ ---
270
+
271
+ ### React
272
+
273
+ ```tsx
274
+ import React, { useEffect, useRef } from 'react';
275
+ import { OFSObject, LegendWidget } from 'ofs-object';
276
+ import 'maplibre-gl/dist/maplibre-gl.css';
277
+
278
+ export const MapView: React.FC = () => {
279
+ const mapContainerRef = useRef<HTMLDivElement>(null);
280
+ const legendRef = useRef<HTMLDivElement>(null);
281
+ const ofsRef = useRef<OFSObject | null>(null);
282
+
283
+ useEffect(() => {
284
+ if (!mapContainerRef.current) return;
285
+
286
+ const ofs = new OFSObject();
287
+ ofsRef.current = ofs;
288
+
289
+ ofs.init(mapContainerRef.current, {
290
+ center: [100.5, 13.7],
291
+ zoom: 12,
292
+ }).then(() => {
293
+ if (legendRef.current) {
294
+ const legendWidget = new LegendWidget(ofs.legendTools);
295
+ legendWidget.mount(legendRef.current);
296
+ }
297
+ });
298
+
299
+ return () => {
300
+ ofs.destroy();
301
+ };
302
+ }, []);
303
+
304
+ return (
305
+ <div style={{ position: 'relative', width: '100%', height: '100vh' }}>
306
+ <div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
307
+ <div ref={legendRef} style={{ position: 'absolute', top: 16, right: 16, zIndex: 10 }} />
308
+ </div>
309
+ );
310
+ };
311
+ ```
312
+
313
+ ---
314
+
315
+ ### Angular
316
+
317
+ ```typescript
318
+ import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
319
+ import { OFSObject } from 'ofs-object';
320
+
321
+ @Component({
322
+ selector: 'app-map-view',
323
+ template: `
324
+ <div class="map-wrapper">
325
+ <div #mapContainer class="map-container"></div>
326
+ <div #consoleContainer class="console-container"></div>
327
+ </div>
328
+ `,
329
+ styles: [`
330
+ .map-wrapper { position: relative; width: 100vw; height: 100vh; }
331
+ .map-container { width: 100%; height: 100%; }
332
+ .console-container { position: absolute; bottom: 20px; left: 20px; width: 500px; z-index: 20; }
333
+ `]
334
+ })
335
+ export class MapViewComponent implements OnInit, OnDestroy {
336
+ @ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
337
+ @ViewChild('consoleContainer', { static: true }) consoleContainer!: ElementRef<HTMLDivElement>;
338
+
339
+ private ofs!: OFSObject;
340
+
341
+ async ngOnInit() {
342
+ this.ofs = new OFSObject();
343
+ await this.ofs.init(this.mapContainer.nativeElement, {
344
+ center: [100.5, 13.7],
345
+ zoom: 13,
346
+ });
347
+ this.ofs.consoleWidget.mount(this.consoleContainer.nativeElement);
348
+ }
349
+
350
+ ngOnDestroy() {
351
+ this.ofs.destroy();
352
+ }
353
+ }
354
+ ```
355
+
356
+ #### Angular: กรณีที่ component สร้างแผนที่เอง
357
+
358
+ ใช้เมื่อโครงการมี map service / component ของตัวเองอยู่แล้ว หรือมีส่วนอื่นที่ต้องใช้ `maplibregl.Map` ตัวเดียวกัน — OFSObject จะเข้าไปใช้ร่วมโดยไม่ยึดความเป็นเจ้าของ:
359
+
360
+ ```typescript
361
+ import { Component, AfterViewInit, OnDestroy, ElementRef, ViewChild, NgZone } from '@angular/core';
362
+ import maplibregl from 'maplibre-gl';
363
+ import { OFSObject } from 'ofs-object';
364
+
365
+ @Component({
366
+ selector: 'app-map-view',
367
+ template: `
368
+ <div #mapContainer class="map-container"></div>
369
+ <div #consoleContainer class="console-container"></div>
370
+ `,
371
+ })
372
+ export class MapViewComponent implements AfterViewInit, OnDestroy {
373
+ @ViewChild('mapContainer') mapContainer!: ElementRef<HTMLDivElement>;
374
+ @ViewChild('consoleContainer') consoleContainer!: ElementRef<HTMLDivElement>;
375
+
376
+ private map!: maplibregl.Map;
377
+ private ofs!: OFSObject;
378
+
379
+ constructor(private readonly zone: NgZone) {}
380
+
381
+ async ngAfterViewInit() {
382
+ // แผนที่เป็นของ component นี้ จะตั้งค่า control / source อื่นเพิ่มเองก็ได้
383
+ this.map = new maplibregl.Map({
384
+ container: this.mapContainer.nativeElement,
385
+ style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
386
+ center: [100.5, 13.7],
387
+ zoom: 13,
388
+ });
389
+ this.map.addControl(new maplibregl.NavigationControl());
390
+
391
+ // เมาส์ทุก event ของแผนที่ไม่ควรไปปลุก change detection ของ Angular
392
+ await this.zone.runOutsideAngular(async () => {
393
+ this.ofs = new OFSObject({ map: this.map });
394
+ await this.ofs.init();
395
+ });
396
+
397
+ this.ofs.consoleWidget.mount(this.consoleContainer.nativeElement);
398
+ }
399
+
400
+ ngOnDestroy() {
401
+ this.ofs.destroy(); // ถอดเฉพาะของ OFSObject ออก แผนที่ยังอยู่
402
+ this.map.remove(); // แผนที่เป็นของเรา จึงลบเอง
403
+ }
404
+ }
405
+ ```
406
+
407
+ > ลำดับสำคัญ: เรียก `ofs.destroy()` **ก่อน** `map.remove()` เสมอ เพื่อให้ถอด source/layer ออกจากแผนที่ที่ยังใช้งานได้อยู่
408
+
409
+ #### Angular: ใช้ Command Engine กับ UI ที่ทำเอง (ไม่ใช้ CSS ของไลบรารี)
410
+
411
+ `consoleWidget` ทำงานได้โดย**ไม่ต้อง `mount()`** — สั่งงานด้วย `submit()` แล้ววาดผลจาก event เอง ไม่ต้องใช้ `ofs-widgets.css` เลย:
412
+
413
+ ```typescript
414
+ export class CommandBarComponent implements OnInit, OnDestroy {
415
+ lines: ConsoleLine[] = [];
416
+ prompt: string | null = null; // ไม่ใช่ null = คำสั่งกำลังรอคำตอบ
417
+ private off: (() => void)[] = [];
418
+
419
+ ngOnInit() {
420
+ const c = this.ofs.consoleWidget!;
421
+ this.lines = [...c.getLines()]; // ข้อความที่เกิดก่อน component นี้ถูกสร้าง
422
+ this.off.push(
423
+ c.on('console:line', (line) => this.zone.run(() => this.lines = [...this.lines, line])),
424
+ c.on('console:cleared', () => this.zone.run(() => this.lines = [])),
425
+ c.on('console:prompt', ({ promptText }) => this.zone.run(() => this.prompt = promptText)),
426
+ );
427
+ }
428
+
429
+ send(text: string) {
430
+ this.ofs.consoleWidget!.submit(text); // ตอบ prompt ที่ค้างอยู่ หรือรันคำสั่งใหม่
431
+ }
432
+
433
+ ngOnDestroy() { this.off.forEach((fn) => fn()); }
434
+ }
435
+ ```
436
+
437
+ `submit()` ฉลาดพอที่จะรู้เองว่าข้อความนั้นคือคำตอบของ prompt ที่ค้างอยู่ หรือเป็นคำสั่งใหม่ และการคลิกแผนที่ตอบ `requestPoint` ก็ยังทำงานตามปกติเพราะ `OFSObject` ต่อ `drawTools` เป็น point provider ให้แล้ว
438
+
439
+ **หรือ**เปลี่ยน console ทั้งตัวเป็นของคุณเอง โดย implement `IOFSConsole` (7 method) แล้วส่งเข้าไป:
440
+
441
+ ```typescript
442
+ const ofs = new OFSObject({ map, console: myAngularConsole });
443
+ // ofs.console คือตัวที่คำสั่งใช้ · ofs.consoleWidget เป็น null
444
+ ```
445
+
446
+ วิธีหลังนี้ `enableDynamicInput()` จะใช้ไม่ได้ (HUD ผูกกับ widget เดิม) ส่วนใหญ่จึงควรใช้วิธีแรก
447
+ >
448
+ > React ใช้รูปแบบเดียวกันใน `useEffect` (สร้าง map ใน ref แล้ว `new OFSObject({ map })`) ส่วน Vue 3 ใช้ใน `onMounted` / `onUnmounted`
449
+
450
+ ---
451
+
452
+ ### Vue 3
453
+
454
+ ```vue
455
+ <script setup lang="ts">
456
+ import { onMounted, onUnmounted, ref } from 'vue';
457
+ import { OFSObject, LegendWidget } from 'ofs-object';
458
+ import 'maplibre-gl/dist/maplibre-gl.css';
459
+
460
+ const mapContainer = ref<HTMLDivElement | null>(null);
461
+ const legendContainer = ref<HTMLDivElement | null>(null);
462
+ let ofs: OFSObject | null = null;
463
+
464
+ onMounted(async () => {
465
+ if (!mapContainer.value) return;
466
+
467
+ ofs = new OFSObject();
468
+ await ofs.init(mapContainer.value, {
469
+ center: [100.5, 13.7],
470
+ zoom: 12,
471
+ });
472
+
473
+ if (legendContainer.value) {
474
+ const legendWidget = new LegendWidget(ofs.legendTools);
475
+ legendWidget.mount(legendContainer.value);
476
+ }
477
+ });
478
+
479
+ onUnmounted(() => {
480
+ ofs?.destroy();
481
+ });
482
+ </script>
483
+
484
+ <template>
485
+ <div class="map-layout">
486
+ <div ref="mapContainer" class="map-container"></div>
487
+ <div ref="legendContainer" class="legend-panel"></div>
488
+ </div>
489
+ </template>
490
+
491
+ <style scoped>
492
+ .map-layout { position: relative; width: 100%; height: 100vh; }
493
+ .map-container { width: 100%; height: 100%; }
494
+ .legend-panel { position: absolute; top: 20px; right: 20px; z-index: 10; }
495
+ </style>
496
+ ```
497
+
498
+ ---
499
+
500
+ ---
501
+
502
+ ## คู่มือฉบับเต็ม
503
+
504
+ ความสามารถทั้งหมดพร้อมตัวอย่างโค้ดทีละส่วนอยู่ใน **[`docs/API.md`](docs/API.md)** ซึ่งมาพร้อมแพ็กเกจ
505
+ เปิดได้จาก `node_modules/ofs-object/docs/API.md`
506
+
507
+ ---
508
+
509
+ ## สัญญาอนุญาต (License)
510
+
511
+ ไลบรารีนี้เผยแพร่ภายใต้สัญญาอนุญาต **[MIT License](LICENSE)** © On-Farm Solution Co.,Ltd.