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/docs/API.md ADDED
@@ -0,0 +1,574 @@
1
+ # OFSObject — คู่มือการใช้งานฉบับเต็ม (API Reference)
2
+
3
+ คู่มือนี้อธิบายความสามารถทั้งหมดของไลบรารีทีละส่วน สำหรับการติดตั้งและตัวอย่างเริ่มต้นดู `README.md`
4
+
5
+ ---
6
+
7
+ ## ภาพรวมสถาปัตยกรรม 5 เสาหลัก
8
+
9
+ ### 1. Data Adapter (`OFSDataSource`)
10
+
11
+ `OFSDataSource` ทำหน้าที่เป็น **Single Source of Truth** จัดเก็บข้อมูล Layer, Feature และสถานะการเลือก (Selection) ทั้งหมด พร้อมระบบ Event Bus แบบ Reactive
12
+
13
+ ```typescript
14
+ // การเพิ่ม Layer
15
+ const layer = ofs.dataSource.addLayer({
16
+ id: 'canals',
17
+ name: 'คลองส่งน้ำ',
18
+ type: 'line',
19
+ zIndex: 2,
20
+ });
21
+
22
+ // เพิ่ม Feature พร้อม Undo Record อัตโนมัติ
23
+ ofs.dataSource.addFeature('canals', lineGeoJsonFeature);
24
+
25
+ // การเลือก Feature บนแผนที่
26
+ ofs.dataSource.selectFeature('canals', 'canal_01');
27
+
28
+ // คำนวณเนื้อที่ (ตารางเมตร, เฮกตาร์ หรือ หน่วย "ไร่")
29
+ const areaRai = ofs.dataSource.calculateArea('parcels', 'plot-1', 'rai');
30
+ console.log(`เนื้อที่: ${areaRai.toFixed(2)} ไร่`);
31
+
32
+ // การสร้างแนวกันชน (Buffer) ระยะ 25 เมตร
33
+ const bufferedPolygon = ofs.dataSource.buffer('canals', 'canal_01', 25, 'meters');
34
+
35
+ // การรวมแปลงที่ดิน (Union) - รวมจุดยอดที่ห่างกันไม่เกิน 1 มม. โดยอัตโนมัติ ไม่มีการ buffer/simplify แฝง
36
+ const merged = ofs.dataSource.union('parcels', ['plot-1', 'plot-2']);
37
+ // หากต้องการปิดรอยห่างที่ใหญ่กว่า ให้กำหนดระยะเอง (เมตร)
38
+ const mergedWithGap = ofs.dataSource.union('parcels', ['plot-3', 'plot-4'], { snapToleranceMeters: 0.2 });
39
+
40
+ // การตัดเฉือนแปลงที่ดิน (Difference)
41
+ const cutResult = ofs.dataSource.difference('parcels', 'plot-1', 'cutter-polygon-id');
42
+
43
+ // ระบบย้อนกลับการทำงาน (Undo / Redo)
44
+ ofs.dataSource.undo(); // ย้อนกลับคำสั่งก่อนหน้า
45
+ ofs.dataSource.redo(); // ทำซ้ำคำสั่งที่ย้อนกลับไป
46
+ ```
47
+
48
+ #### เหตุการณ์ที่สามารถดักฟังได้ (Events)
49
+ ```typescript
50
+ ofs.dataSource.on('feature:added', ({ layerId, feature }) => { ... });
51
+ ofs.dataSource.on('feature:updated', ({ layerId, feature }) => { ... });
52
+ ofs.dataSource.on('selection:changed', ({ layerId, featureIds }) => { ... });
53
+ ofs.dataSource.on('layer:updated', ({ layer }) => { ... });
54
+ ```
55
+
56
+ ---
57
+
58
+ ### 2. Draw Tools, OSnap & CAD Operations
59
+
60
+ เครื่องมือวาดเรขาคณิตความแม่นยำสูงระดับ CAD มาพร้อมระบบช่วยจับเส้น/จุดอัตโนมัติ และระบบเลือกวัตถุ (Selection System)
61
+
62
+ ```typescript
63
+ // เปิดโหมดการวาดและเลือกวัตถุ
64
+ ofs.drawTools.setMode('polygon'); // 'point' | 'line' | 'polygon' | 'circle' | 'rectangle' | 'select' | 'select-box' | 'select-polygon' | 'modify'
65
+
66
+ // 1. ระบบเลือกวัตถุ (Selection System) และการแยก Mode ชัดเจน:
67
+ // - โหมด Select ('select'): คลิกเลือกวัตถุชิ้นเดียว แสดงไฮไลต์สีฟ้า Neon Cyan ทันที (ไม่เปิดจุด Grips รบกวนหน้าจอ)
68
+ // - โหมด Modify ('modify'): ดับเบิลคลิกที่วัตถุ หรือกดปุ่ม Grips Editor เพื่อเข้าสู่โหมดดัดจุดแก้ไขรูปทรงเรขาคณิต
69
+ // - Shift + Click: เลือกวัตถุแบบ Multi-selection หรือ Toggle เลือกเพิ่ม/ยกเลิกชิ้นนั้น
70
+ // - คลิกที่ว่างขณะกด Shift: รักษาสถานะวัตถุที่เลือกไว้เดิม (ไม่ Clear การเลือก)
71
+ // - คลิกที่ว่างแบบปกติ (ไม่มี Shift): ยกเลิกการเลือกทั้งหมด (Clear Selection)
72
+
73
+ // 2. โหมด Draw Selection (เลือกพื้นที่ด้วยกรอบหรือโพลีกอน):
74
+ ofs.drawTools.setMode('select-box'); // คลิก 2 มุมทแยง หรือคลิกลากเมาส์ (Marquee Box) เพื่อเลือกวัตถุ
75
+ ofs.drawTools.setMode('select-polygon'); // คลิกสร้างขอบเขตโพลีกอน แล้ว Double-click หรือ Enter เพื่อเลือกวัตถุ
76
+ // * Draw Selection จะตรวจจับวัตถุในทุก Visible Layer และไม่เพิ่มรูปทรงลงฐานข้อมูล
77
+
78
+ // 3. ระบบแสดงผล Interactive Feedback & Selection Overlay บนแผนที่:
79
+ // - High-Visibility Neon Cyan Glow (#00e5ff): แสงเรืองรอบวัตถุที่ถูกเลือก ชัดเจนทุกเลเยอร์แม้ใช้ Thematic สีใด
80
+ // - Multi-selection Bounding Box: กรอบเส้นประสีฟ้าโอบล้อมวัตถุทั้งหมดเมื่อเลือก 2 ชิ้นขึ้นไป
81
+ // - Live Drag Progress HUD: คำนวณขนาดกว้าง x ยาว (เมตร) และจำนวนวัตถุที่คลุมแบบ Real-time
82
+ // - Hover Aura & Pointer Cursor: แสงไฮไลต์สีฟ้าอ่อนนำสายตาเมื่อเลื่อนเมาส์ชี้วัตถุในโหมด Select / Modify
83
+ // - CAD Confirmation Flash (#22c55e): กะพริบแสงสีเขียวทันทีเมื่อคำสั่ง CAD (Offset, Split, Union ฯลฯ) ทำงานเสร็จสิ้น
84
+ ofs.drawTools.flashFeatures([resultFeature], 750); // กะพริบไฮไลต์ยืนยันผลการแปลงพิกัด
85
+
86
+ ofs.drawTools.on('selection:drag-progress', ({ widthMeters, heightMeters, candidateCount }) => {
87
+ console.log(`ขนาดกรอบ: ${widthMeters}m x ${heightMeters}m, วัตถุที่พบ: ${candidateCount}`);
88
+ });
89
+
90
+ ofs.drawTools.on('selection:draw-complete', ({ mode, selectedFeatures, count }) => {
91
+ console.log(`เลือกวัตถุสำเร็จ ${count} รายการ`, selectedFeatures);
92
+ });
93
+
94
+ // ยกเลิกการวาดปัจจุบัน
95
+ ofs.drawTools.cancel();
96
+
97
+ // สั่งจบการวาด (หรือผู้ใช้กด Enter / Double-click)
98
+ ofs.drawTools.finishDrawing();
99
+
100
+ // การตั้งค่าระบบจับวัตถุ AutoCAD OSnap พร้อมตัวนำสายตา Soft Blue Glow Halo
101
+ ofs.drawTools.snapping.enabled = true;
102
+ ofs.drawTools.snapping.tolerancePixels = 18; // รัศมีตรวจจับพิกัด (Default: 18px)
103
+ ofs.drawTools.snapping.setModes({
104
+ vertex: true, // ล็อกที่จุดมุม (Vertex Halo Indicator สีฟ้า)
105
+ midpoint: true, // ล็อกที่กึ่งกลางเส้น (Midpoint Indicator สีฟ้าสว่าง)
106
+ edge: true, // ล็อกบนเส้นขอบ (Edge Indicator สีม่วง)
107
+ });
108
+
109
+ // เปิด/ปิดโหมด Ortho Mode (F8 - ล็อกแกน 90 องศา)
110
+ ofs.drawTools.toggleOrtho();
111
+
112
+ // เปิด AutoCAD Grips Editor บนแปลงที่เลือก
113
+ ofs.drawTools.startEdit('parcels', 'plot-1');
114
+ // - ลากจุดสีชมพู (Vertex Grip): ปรับดัดมุม (รองรับ OSnap Snap Guide ขณะลาก)
115
+ // - ลากจุดชมพูเล็ก (Midpoint Grip): เพิ่มจุดมุมใหม่กึ่งกลางเส้น (รองรับ OSnap Snap Guide ขณะลาก)
116
+ // - ลากจุดสีเขียว (Center Grip): ย้ายตำแหน่งทั้งรูปทรง
117
+ // - กดปุ่ม Delete / Backspace: ลบจุดมุมที่กำลังเลือก
118
+ // - หมายเหตุรอยต่อแปลง: เมื่อวาดหรือลาก Snap ชนกัน จุดพิกัดจะแนบสนิท 100%
119
+ // เส้นสีขาวที่เห็นระหว่างแปลงคือขอบเส้น Outline Stroke (${layerId}_stroke) ที่ระบบสร้างขึ้นอัตโนมัติ
120
+ ```
121
+
122
+ #### การใช้ฟังก์ชัน CAD ขั้นสูง (`CADOperations`)
123
+ ```typescript
124
+ import { CADOperations } from 'ofs-object';
125
+
126
+ // 1. ตัดแบ่งแปลงด้วยแนวเส้น (Split Polygon by Line - Zero-Gap Planar Graph Noded Polygonize รักษาแนวเส้นต่อเนื่อง ไม่แตกกระจายเป็นชิ้นย่อย)
127
+ const pieces = CADOperations.splitPolygonByLine(parcelFeature, cuttingLineFeature);
128
+
129
+ // 2. แยก Polygon ตามขอบที่ซ้อนทับกัน (Split Overlapping Polygons: A\B, A∩B, B\A)
130
+ const disjointParts = CADOperations.splitOverlappingPolygons(polyA, polyB);
131
+
132
+ // 3. รวมแปลงพร้อมกำจัดจุดยอดส่วนเกินบนขอบตรง (Clean Collinear Vertices)
133
+ const cleanedPoly = CADOperations.cleanCollinearVertices(unionedPoly);
134
+
135
+ // 4. ตัดแบ่งเส้น LineString (Split / Break Line)
136
+ const linePieces = CADOperations.splitLine(lineFeature, cuttingPointOrLine);
137
+
138
+ // 5. เชื่อมต่อเส้นโดยรักษาทิศทางของเส้นแรกเป็นหลัก (Connect Lines - 1st Line Priority)
139
+ const connectedLine = CADOperations.connectLines([line1, line2], 5 /* tolerance เมตร */, true);
140
+
141
+ // 6. กลับทิศทางของเส้น (Reverse Line)
142
+ const reversedLine = CADOperations.reverse(lineFeature);
143
+
144
+ // 7. ปรับเส้นให้โค้งมนเรียบด้วย Bezier Curve
145
+ const smoothLine = CADOperations.smoothLineBezier(lineFeature, 32);
146
+
147
+ // 8. สร้างเส้นขนาน / ขยายขอบเขต (Offset)
148
+ const offsetPoly = CADOperations.offset(parcelFeature, 5 /* เมตร */);
149
+
150
+ // 9. หมุนรูปทรง (Rotate)
151
+ const rotated = CADOperations.rotate(parcelFeature, 45 /* องศา */);
152
+
153
+ // 10. ปรับขนาดรูปทรง (Scale)
154
+ const scaled = CADOperations.scale(parcelFeature, 1.5 /* เท่า */);
155
+
156
+ // 11. ลบเหลี่ยมมุมมน (Fillet)
157
+ const rounded = CADOperations.fillet(parcelFeature, 0 /* vertex index */, 10 /* รัศมี เมตร */); // null หากรัศมีใหญ่เกิน
158
+ const maxRadius = CADOperations.maxFilletRadius(parcelFeature, 0);
159
+
160
+ // 12. ระเบิดรูปแปลงเป็นเส้นรอบรูปทีละด้าน (Explode)
161
+ const segments = CADOperations.explode(parcelFeature);
162
+ ```
163
+
164
+ ---
165
+
166
+ ### 3. Legend & Thematic Symbology
167
+
168
+ `OFSLegendTools` และ `OFSThematicEngine` ทำหน้าที่ประมวลผลการจัดจำแนกข้อมูลทางสถิติ และสร้างสไตล์การแสดงผลเป็น Mapbox/MapLibre GL Expressions โดยอัตโนมัติ
169
+
170
+ ```typescript
171
+ // 1. สไตล์แบบสีเดียวเรียบง่าย (Simple)
172
+ ofs.legendTools.setThematicSimple('parcels', '#0284c7');
173
+
174
+ // 2. จำแนกข้อมูลแบบกลุ่มข้อความ (Categorical / Unique Values)
175
+ // เหมาะสำหรับประเภทพืช, พันธุ์ข้าว, ชนิดดิน
176
+ ofs.legendTools.setThematicCategorical('parcels', 'crop', 'category10');
177
+
178
+ // 3. จำแนกข้อมูลแบบชั้นปริมาณ (Graduated / Choropleth)
179
+ // เหมาะสำหรับค่า NDVI, ผลผลิต (kg/ไร่), ความชื้น
180
+ ofs.legendTools.setThematicGraduated(
181
+ 'parcels',
182
+ 'ndvi',
183
+ 'natural_breaks', // 'quantile' | 'equal_interval' | 'natural_breaks' (Jenks) | 'pretty_breaks'
184
+ 5, // จำนวน Class
185
+ 'YlGn', // ชุดสี ColorBrewer / CARTO / viridis (ดูทั้งหมด: listColorRamps() หรือคำสั่ง `symbology ramps`)
186
+ { reverse: false, mode: 'color' } // mode 'size' = ไล่ความหนาเส้น / ขนาดจุด (เลเยอร์เส้นและจุด)
187
+ );
188
+
189
+ // 4. Symbol แบบ QGIS: ลายแรเงา, เส้นประ, รูปทรง marker (คงอยู่เมื่อสลับ renderer)
190
+ ofs.legendTools.updateBaseSymbol('parcels', {
191
+ fill: { style: 'pattern', pattern: { kind: 'lines', color: '#166534', spacing: 8, angleDeg: 45, lineWidth: 1 }, outlineDash: [4, 2] },
192
+ });
193
+ ofs.legendTools.updateBaseSymbol('canals', { line: { width: 3, dash: [8, 4] } });
194
+ ofs.legendTools.updateBaseSymbol('wells', { marker: { shape: 'star', size: 18, strokeColor: '#fff', strokeWidth: 1.5 } });
195
+ registerMarkerIcon('pump', '<svg ...>...</svg>'); // ไอคอนของตัวเอง → { shape: 'icon', icon: 'pump' }
196
+
197
+ // 5. ทีละ class: ซ่อน/แสดง (ซ่อนแล้วคลิกไม่โดนด้วย), แก้สี/ป้าย/ขอบ class (ขอบ class ข้างเคียงเลื่อนตาม)
198
+ ofs.legendTools.setClassVisible('parcels', 2, false); // index -1 = "Other values"
199
+ ofs.legendTools.updateClass('parcels', 1, { max: 0.45, label: 'ต่ำ' });
200
+
201
+ // 6. กลุ่มเลเยอร์ (ซ้อนกันได้) และปักหมุด
202
+ const water = ofs.legendTools.addGroup('แหล่งน้ำ');
203
+ ofs.legendTools.moveLayerToGroup('canals', water);
204
+ ofs.legendTools.toggleGroupVisibility(water);
205
+ ofs.legendTools.toggleLayerPinned('parcels');
206
+
207
+ // การปรับความโปร่งใสและลำดับเลเยอร์
208
+ ofs.legendTools.setLayerOpacity('parcels', 0.8);
209
+ ofs.legendTools.moveLayerUp('parcels');
210
+ ofs.legendTools.toggleLayerVisibility('parcels');
211
+
212
+ // บันทึก/โหลดสไตล์ทั้งหมดเป็น JSON
213
+ const saved = ofs.thematicEngine.exportThemes();
214
+ ofs.thematicEngine.importThemes(saved);
215
+ ```
216
+
217
+ > **กฎการจัด class (เหมือน QGIS):** ค่า v อยู่ใน class เมื่อ `min < v <= max` (class แรกรวม `min`) — แผนที่ จำนวนใน legend และตัวจัด class ใช้กฎเดียวกันทุกที่ ส่วน Categorized แยกชนิดข้อมูล (`1` กับ `"1"` เป็นคนละค่า)
218
+
219
+ #### Drop-in Legend & Symbology Widgets
220
+ ```typescript
221
+ import { LegendWidget, OFSSymbologyPanel } from 'ofs-object';
222
+
223
+ const panel = new OFSSymbologyPanel(ofs.legendTools, { onClose: () => dialog.hidden = true });
224
+ panel.mount('#symbology-dialog');
225
+
226
+ const widget = new LegendWidget(ofs.legendTools, {
227
+ onEditSymbology: (layerId) => { panel.open(layerId); dialog.hidden = false; }, // ปุ่ม 🎨
228
+ });
229
+ widget.mount('#my-legend-panel');
230
+ ```
231
+ Legend แสดงกลุ่มเลเยอร์, ปุ่มปักหมุด 📌, รูปตัวอย่างสัญลักษณ์ (SVG) และ checkbox ทีละ class พร้อมจำนวนฟีเจอร์ ส่วนแผง Symbology มี Renderer / Value / Color ramp / Method / Classes / Mode, ตาราง class และตัวแก้ symbol ของ polygon, line, point
232
+
233
+ ---
234
+
235
+ ### 3.5 นำเข้า / ส่งออกไฟล์ (`ofs.io`)
236
+
237
+ รองรับ **GeoJSON, Shapefile (.zip หรือ .shp+.dbf+.prj+.cpg), GeoPackage (.gpkg + .gpkg-wal), KML, KMZ, CSV (lat/lon, E/N หรือ WKT) และ DXF** ข้อมูลในระบบเก็บเป็น WGS84 เสมอ และแปลงพิกัดตอนนำเข้า/ส่งออก
238
+
239
+ ```typescript
240
+ // นำเข้า (ในเบราว์เซอร์: File จาก <input type="file"> หรือ drag & drop)
241
+ const result = await ofs.io.importFileList(input.files, {
242
+ sourceCrs: 'EPSG:32647', // ใช้เมื่อไฟล์ไม่มีระบบพิกัด (CSV, DXF, shapefile ที่ไม่มี .prj)
243
+ encoding: 'tis-620', // ใช้เมื่อ DBF/CSV ไม่มี .cpg และตรวจเองไม่ได้
244
+ });
245
+ result.layers; // [{ layerId, name, featureCount, sourceCrs }]
246
+ result.issues; // ทุกอย่างที่ข้าม/แปลง/สันนิษฐาน: { severity, code, message, featureIndex }
247
+
248
+ // ส่งออก
249
+ const { file, issues } = ofs.io.export(['plots'], 'shapefile', { crs: 'EPSG:24047', encoding: 'tis-620' });
250
+ OFSFileIO.download(file);
251
+ ```
252
+
253
+ | เรื่อง | พฤติกรรม |
254
+ |---|---|
255
+ | ระบบพิกัด | อ่านจาก `.prj` / สมาชิก `crs` ของ GeoJSON / ตัวเลือก `sourceCrs`; มี WGS84, UTM 46–48N, **Indian 1975 / UTM 47N–48N (EPSG:24047/24048)**, Web Mercator; เพิ่มเองได้ด้วย `registerCrs()` |
256
+ | Indian 1975 | `.prj` แบบ ESRI ไม่มีค่าแปลง datum — ระบบใช้ค่า 3 พารามิเตอร์ WGS84→Indian 1975 = ΔX −204.4798, ΔY −837.8940, ΔZ −294.7765 ม. (proj4 `+towgs84=204.4798,837.894,294.7765` คือทิศกลับ) **พร้อมคำเตือน** เปลี่ยนได้ด้วย `setDatumShift('indian_1975', '...')` |
257
+ | พิกัดที่ดูเป็นเมตรแต่ไม่มีระบบพิกัด | **ไม่นำเข้า** และบอกให้ระบุ `sourceCrs` (ไม่วางข้อมูลผิดที่เงียบ ๆ) |
258
+ | ภาษาไทยใน DBF / CSV | อ่านจาก `.cpg` → ไบต์ LDID → ตรวจอัตโนมัติ (UTF-8 / TIS-620) และแจ้งเมื่อเดา; ส่งออก UTF-8 (ค่าเริ่มต้น) หรือ TIS-620 |
259
+ | Geometry ผิด | ring ไม่ปิด, พิกัดไม่ใช่ตัวเลข, จุดไม่พอ → ข้ามฟีเจอร์นั้นและรายงาน (`closeRings: true` เพื่อปิด ring ให้); เส้นตัดตัวเอง → นำเข้าแต่เตือน |
260
+ | DXF | เลเยอร์ AutoCAD → เลเยอร์ OFS, arc/bulge/circle แปลงเป็นจุด (ไม่เกิน 5°/ช่วง), TEXT/MTEXT เป็นจุดพร้อมข้อความ, ส่งออก R12 ในระบบ UTM ของข้อมูล |
261
+ | GeoPackage | อ่าน/เขียนด้วยตัวอ่าน SQLite ที่เขียนเป็น TypeScript (ไม่ใช้ WASM), หลายเลเยอร์ต่อไฟล์, รวมการเปลี่ยนแปลงใน `.gpkg-wal` ถ้านำเข้ามาด้วย; ไม่เขียน spatial index (QGIS/GDAL เปิดได้) |
262
+ | Indian 1975 ใน GeoPackage | เขียนเป็น CRS ที่ระบุค่าแปลง datum ของโครงการ (srs_id 124047/124048) เพราะถ้าระบุแค่ EPSG:24047 GDAL/QGIS จะใช้การแปลงแบบ 7 พารามิเตอร์ของ EPSG ซึ่งต่างจากค่าโครงการ ~190 ม. |
263
+ | ข้อจำกัด | ชื่อฟิลด์ DBF ≤ 10 ตัวอักษร, ข้อความ ≤ 254 ไบต์ (แจ้งเมื่อตัด), DXF R12 ไม่มี attribute, GeoPackage ยังไม่รองรับเส้นโค้ง (CurvePolygon ฯลฯ) และตาราง attributes/tiles |
264
+
265
+ ### 3.6 ตรวจความถูกต้อง Topology (`ofs.topology`)
266
+
267
+ ตรวจรูปทรงที่ผิดกติกา, แปลงซ้อนกัน, ช่องว่างระหว่างแปลง, แปลงบางผิดปกติ และจุดยอดที่เกือบชนกัน แล้ว**แก้เมื่อสั่งเท่านั้น** (ทุกการแก้เป็น undo ครั้งเดียว)
268
+
269
+ ```typescript
270
+ const report = ofs.topology.check('plots', {
271
+ nearToleranceMeters: 0.05, // จุดยอดห่างขอบแปลงอื่นไม่เกิน 5 ซม. = เกือบชน (ค่าเริ่มต้น 0.1 ม.)
272
+ areaToleranceSqm: 0.01, // ไม่รายงานส่วนซ้อน/ช่องว่างที่เล็กกว่านี้
273
+ minAreaSqm: 10, // แจ้งแปลงที่เล็กกว่า 10 ตร.ม. (ค่าเริ่มต้น: ปิด)
274
+ checks: ['validity', 'overlap', 'gap', 'sliver', 'near-vertex'],
275
+ });
276
+ report.issues; // [{ id, code, severity, featureIds, message, location, geometry, areaSqm, fix }]
277
+ report.summary; // { overlap: 3, gap: 1, ... }
278
+ report.ignored; // สิ่งที่ต่ำ/สูงกว่าเกณฑ์ (ไม่หายไปเงียบ ๆ)
279
+
280
+ ofs.topology.showIssues('plots'); // เลเยอร์ปัญหาสีแดงในกลุ่ม "Topology errors"
281
+ ofs.mapAdapter.fitBounds(ofs.topology.issueBBox(report.issues[0]), 60);
282
+
283
+ ofs.topology.fixIssue(1, { removeFrom: 'smaller' }); // ตัดส่วนซ้อนออกจากแปลงที่เล็กกว่า
284
+ ofs.topology.fixIssue(2, { mergeInto: 'longest-boundary' }); // รวมช่องว่างเข้ากับแปลงที่ขอบติดยาวที่สุด
285
+ ofs.topology.fixIssues([3, 4, 5]); // snap จุดยอดหลายรายการใน undo เดียว
286
+ ofs.dataSource.undo();
287
+ ```
288
+
289
+ | เรื่อง | พฤติกรรม |
290
+ |---|---|
291
+ | รูปทรงผิด | ใช้กฎ OGC ผ่าน JTS: เส้นขอบตัดตัวเอง, รูอยู่นอกวงนอก, รูซ้อนรู, รูแยกพื้นที่ออกจากกัน ฯลฯ พร้อมจุดตัดทุกจุด; เส้นที่ตัดตัวเองเป็นคำเตือน |
292
+ | แปลงซ้อนกัน | รายงานเป็นคู่พร้อมพื้นที่และความกว้างเฉลี่ย, รูปร่างซ้ำกันแจ้งเป็น `duplicate-geometry` |
293
+ | ช่องว่าง | รูของ union ทั้งเลเยอร์ที่มีแปลงล้อมตั้งแต่ 2 แปลง (รูของแปลงเดียว เช่น บ่อในแปลง ไม่นับ) |
294
+ | จุดเกือบชน | จุดยอดหรือปลายเส้นที่ห่างขอบของอีกฟีเจอร์เกิน 1 มม. แต่ไม่เกิน tolerance — ต้นเหตุของช่องว่าง/ส่วนซ้อนบาง ๆ |
295
+ | การแก้ | snap จุดยอด (ย้ายไปจุดยอดก่อน แล้วแทรกจุดของอีกแปลงให้ขอบใช้จุดชุดเดียวกัน), ตัดส่วนซ้อน, เติมช่องว่าง, ลบจุดซ้ำ — ถ้าผลจะกลายเป็นรูปทรงผิดหรือยุบ จะไม่แก้และแจ้งเหตุผล |
296
+ | ความปลอดภัย | ถ้าข้อมูลถูกแก้หลังตรวจ จะไม่ยอมแก้จนกว่าจะตรวจใหม่ (ป้องกันแก้ผิดชิ้น) |
297
+ | ข้อจำกัด | ช่องว่างที่เปิดออกด้านนอกไม่เข้าเงื่อนไข "รูของ union" (ตรวจได้ทางอ้อมจากจุดเกือบชน), ยังไม่มีกฎ topology ต่อเลเยอร์และยังไม่ตรวจข้ามเลเยอร์ |
298
+
299
+ แผง UI: `OFSTopologyPanel` (เลือกเลเยอร์/ค่า tolerance, กด Check, คลิกรายการเพื่อซูมไปจุดที่ผิด, ปุ่ม 🛠 เพื่อแก้) และคำสั่ง `topology` ใน Command Line
300
+
301
+ ### 3.7 ตาราง Attribute และเครื่องคิดเลขฟิลด์ (`ofs.attributes`)
302
+
303
+ Schema ของฟิลด์ (ชนิด, ชื่อแสดง, ค่าเริ่มต้น, ค่าที่อนุญาต, ขอบเขต), ตาราง attribute, เครื่องคิดเลขฟิลด์, เลือกด้วยเงื่อนไขหรือตำแหน่ง และสถิติ — ทุกการแก้เป็น undo ครั้งเดียวและค่าที่ไม่ตรงชนิดจะถูกปฏิเสธ ไม่ดัดให้เข้าเกณฑ์เอง
304
+
305
+ ```typescript
306
+ // Schema
307
+ ofs.attributes.getSchema('plots'); // [{ name, type, alias, defaultValue, allowedValues, ... }]
308
+ ofs.attributes.addField('plots', { name: 'ไร่', type: 'number', precision: 2 }, { expression: 'round($area / 1600, 2)' });
309
+ ofs.attributes.updateField('plots', 'crop', { alias: 'ชนิดพืช', allowedValues: ['ข้าว', 'อ้อย', 'มันสำปะหลัง'] });
310
+
311
+ // ตาราง (กรอง / เรียง / เฉพาะที่เลือก / แบ่งหน้า)
312
+ const { rows, filtered, total } = ofs.attributes.query('plots', { filter: "crop = 'ข้าว' AND $area > 1600", sortBy: 'ไร่', sortDescending: true, limit: 50 });
313
+ ofs.attributes.setValue('plots', 'p1', 'crop', 'อ้อย'); // ตรวจกับ schema ก่อนเขียน
314
+
315
+ // เครื่องคิดเลขฟิลด์ · เลือก · สถิติ
316
+ ofs.attributes.calculateField('plots', 'ขนาดที่ดิน', 'rai_ngan_wa()', { createField: true });
317
+ ofs.attributes.selectByExpression('plots', "owner LIKE 'สม%' AND year >= 2025");
318
+ ofs.attributes.selectByLocation('plots', 'within-distance', 'canals', { distanceMeters: 50 });
319
+ ofs.attributes.statistics('plots', 'yield'); // count, min, max, sum, mean, median, stdDev
320
+ ```
321
+
322
+ **ภาษา expression** (ใช้ได้ทั้งตัวกรอง, `select` และเครื่องคิดเลขฟิลด์) เขียนแบบ QGIS:
323
+
324
+ | กลุ่ม | ตัวอย่าง |
325
+ |---|---|
326
+ | ฟิลด์ / ข้อความ | `crop`, `"ชื่อ มีเว้นวรรค"`, `'ข้าว'` |
327
+ | ตัวดำเนินการ | `= <> < <= > >= + - * / % ^ \|\|` · `AND OR NOT` · `IN ('ข้าว','อ้อย')` · `LIKE 'A%'` / `ILIKE` · `IS NULL` |
328
+ | ค่าเรขาคณิต | `$area` (ตร.ม.), `$perimeter`, `$length`, `$x`, `$y`, `$id`, `area('rai')`, `perimeter('km')` |
329
+ | หน่วยไทย | `area('rai')`, `area('ngan')`, `area('wa')`, `rai_ngan_wa()` → `"3-2-45.5"` (1 ไร่ = 4 งาน = 400 ตร.วา = 1,600 ตร.ม.) |
330
+ | ฟังก์ชัน | `round(v, 2)`, `abs`, `min`, `max`, `concat`, `upper/lower/trim`, `substr`, `replace`, `coalesce`, `if(cond, a, b)`, `to_int/to_real/to_string`, `today()` |
331
+
332
+ | เรื่อง | พฤติกรรม |
333
+ |---|---|
334
+ | ความปลอดภัย | นิพจน์ถูกแปลงเป็น AST เอง **ไม่ใช้ `eval`** ชื่อที่ไม่รู้จักแจ้ง error พร้อมรายชื่อฟิลด์/ฟังก์ชันที่มี |
335
+ | ค่าว่าง | ใช้กฎแบบ SQL: คำนวณกับ NULL ได้ NULL, `NULL OR TRUE` = TRUE, ตัวกรองถือว่า NULL ไม่ผ่าน; ค่าว่างเรียงท้ายสุดทั้งสองทิศ |
336
+ | การตรวจค่า | ข้อความยาวเกิน, ค่านอกรายการที่อนุญาต, วันที่ไม่มีจริง (2026-02-30) → ปฏิเสธพร้อมเหตุผล ไม่ตัด/ไม่ปัดให้ |
337
+ | เครื่องคิดเลขฟิลด์ | ถ้ามีฟีเจอร์ใดคำนวณไม่ได้ **จะไม่เขียนอะไรเลย** (ใส่ `skipErrors: true` เพื่อข้ามเฉพาะตัวที่ผิด) |
338
+ | เลือกตามตำแหน่ง | `intersects`, `within`, `contains`, `disjoint`, `touches`, `crosses`, `within-distance` (ระยะเป็นเมตรบนพื้นจริง) |
339
+
340
+ แผง UI: `OFSAttributeTable` (เรียงจากหัวคอลัมน์, ช่องกรอง, ดับเบิลคลิกแก้ค่า, คลิกแถว = เลือกบนแผนที่, ปุ่ม `ƒx` เครื่องคิดเลขฟิลด์, `Σ` สถิติ)
341
+
342
+ ### 3.8 Label, เครื่องมือวัด และบันทึกภาพแผนที่
343
+
344
+ ```typescript
345
+ // Label จากฟิลด์ หรือจากนิพจน์ (คำนวณให้ก่อนส่งเข้าแผนที่ จึงใช้ $area / rai_ngan_wa() ได้)
346
+ ofs.legendTools.setLabel('plots', { field: 'owner', size: 13, haloColor: '#fff', minZoom: 14 });
347
+ ofs.legendTools.setLabel('plots', { expression: "concat(owner, ' ', rai_ngan_wa())" });
348
+ ofs.legendTools.setLabel('plots', null); // เอาป้ายออก
349
+
350
+ // วัดระยะ / พื้นที่ / มุม (ระยะบนพื้นจริง)
351
+ import { measureDistance, measureArea, measureAngle, formatArea } from 'ofs-object';
352
+ measureArea([[100.5, 13.75], [100.501, 13.75], [100.501, 13.751]]).raiNganWa; // "0-1-53.4"
353
+ formatArea(4000); // "4,000.00 m² · 2.50 rai · 2-2-0.0 (rai-ngan-wa)"
354
+
355
+ // บันทึกภาพแผนที่พร้อมมาตราส่วนและลูกศรทิศเหนือ (ต้องสร้างแผนที่ด้วย preserveDrawingBuffer: true)
356
+ import { exportMapImage, downloadMapImage } from 'ofs-object';
357
+ downloadMapImage(exportMapImage(ofs.mapAdapter, { width: 1600, title: 'แผนที่แปลง', attribution: '© OpenStreetMap' }));
358
+ ```
359
+
360
+ | เรื่อง | พฤติกรรม |
361
+ |---|---|
362
+ | Label | ฟิลด์แปลงเป็น expression ของ GL ตรง ๆ (เปลี่ยนขนาด/สีไม่ต้องส่งข้อมูลใหม่); นิพจน์คำนวณในไลบรารีแล้วส่งเป็น property `_ofs_label` และส่งข้อมูลใหม่เฉพาะตอนนิพจน์เปลี่ยน |
363
+ | ตัวเลือก Label | `size`, `color`, `haloColor`, `haloWidth`, `offset`, `anchor`, `placement` (`point` / `line`), `decimals`, `minZoom` / `maxZoom`, `allowOverlap`, `transform`, `font`, `priorityField` |
364
+ | การวัด | ระยะ/พื้นที่/มุม คำนวณบนระนาบ TM เฉพาะที่ (ระยะบนพื้นจริง) มุมรายงานตามระบบมุมของ console (bearing เป็นค่าเริ่มต้น) เป็นองศา-ลิปดา-ฟิลิปดา |
365
+ | หน่วยไทย | 1 ไร่ = 4 งาน = 400 ตร.วา = 1,600 ตร.ม.; การปัด **ทดขึ้น**อย่างถูกต้อง (1,599.99 ตร.ม. = `1-0-0.0` ไม่ใช่ `0-3-100.0`) |
366
+ | ภาพแผนที่ | มาตราส่วนเลือกระยะกลม 1/2/5 × 10ⁿ เมตร, ลูกศรทิศเหนือหมุนตาม bearing, ใส่ชื่อแผนที่และเครดิตแผนที่ฐานได้; ถ้าแผนที่ไม่ได้เปิด `preserveDrawingBuffer` จะแจ้งเตือนแทนการบันทึกภาพเปล่า |
367
+
368
+ #### พารามิเตอร์ของคำสั่งมาจาก Schema เดียว
369
+
370
+ ทุกคำสั่งประกาศพารามิเตอร์ครั้งเดียว ระบบจึงรับได้ทั้งแบบพิมพ์, แบบ `--option` และแบบ JSON ของ AI:
371
+
372
+ ```bash
373
+ buffer 25 # ตามลำดับ
374
+ buffer --distance 25 --units meters
375
+ buffer {"distance": 25} # รูปแบบที่ AI ส่งมา
376
+ help buffer # พารามิเตอร์ทั้งหมดของคำสั่ง (สร้างจาก schema)
377
+ ```
378
+
379
+ - ใช้ได้กับ **ทุกคำสั่ง** — วาด, CAD, symbology, import/export, topology, attribute, measure และ print เดินผ่านตัวแปลงเดียวกัน
380
+ - ชื่อ option ใช้ได้ทั้ง `--minZoom` และ `--min-zoom`; ค่า boolean เป็น flag (`--dry-run`) ; list รับ `a,b,c` หรือ JSON; option สั้นหนึ่งตัวอักษรใช้ได้ที่คำสั่งที่กำหนดไว้ (`offset 15 -c`)
381
+ - **พิกัดใช้ลำดับเดียวคือ `lng,lat`** (ลองจิจูดก่อน เหมือน GeoJSON) ทั้งการพิมพ์คำสั่ง การพิมพ์จุดขณะวาด และการแสดงผล — `line 100.5,13.7 100.6,13.8` เท่ากับ `line [[100.5, 13.7], [100.6, 13.8]]` ส่วนค่าที่สลับลำดับจะถูกปฏิเสธพร้อมบอกค่าที่ถูก (`"13.7, 100.5" is not lng,lat — longitude comes first; did you mean "100.5, 13.7"?`) ไม่สลับให้เงียบ ๆ
382
+ - คำสั่งที่รับนิพจน์จะเก็บข้อความตามที่พิมพ์ทุกตัวอักษร: `select "crop = 'ข้าว' AND $area > 1600"` และ `calc rai = round($area / 1600, 2) --selected`
383
+ - ตัวย่อที่แทนค่าเต็ม เช่น `select … --add` (เท่ากับ `--mode add`), `export plots csv --xy`, `print --no-scale`
384
+ - ค่าที่ผิดจะถูกปฏิเสธพร้อมบอกสิ่งที่รับได้ เช่น `units must be one of meters, kilometers (got "furlongs")` และ `unknown option --row; did you mean --rows?`
385
+ - AI ที่ส่ง argument ไม่ครบจะได้คำตอบกลับพร้อมรายการพารามิเตอร์ของคำสั่งนั้น เพื่อเรียกใหม่ให้ถูก
386
+ - กด **Tab** ในคอนโซลเพื่อเติมชื่อคำสั่ง ชื่อ `--option` หรือดูพารามิเตอร์ของคำสั่งที่พิมพ์ค้างไว้
387
+
388
+ #### Macro: บันทึกชุดคำสั่งแล้วเล่นซ้ำ
389
+
390
+ ```bash
391
+ macro record ตรวจแปลง
392
+ topology check plots --near 0.05 --show
393
+ calc ไร่ = round($area / 1600, 2) --create
394
+ macro stop
395
+ macro play ตรวจแปลง --times 2
396
+ ```
397
+
398
+ ### 4. Map Adapters (MapLibre & Mapbox)
399
+
400
+ รองรับทั้ง MapLibre GL และ Mapbox GL ด้วย Interface เดียวกัน:
401
+
402
+ ```typescript
403
+ import { MapLibreAdapter, MapboxAdapter } from 'ofs-object';
404
+
405
+ // สำหรับ MapLibre GL JS
406
+ const mapAdapter = new MapLibreAdapter();
407
+
408
+ // หรือสำหรับ Mapbox GL JS
409
+ const mapAdapter = new MapboxAdapter();
410
+
411
+ await mapAdapter.init('map-div', {
412
+ accessToken: 'YOUR_MAPBOX_TOKEN',
413
+ style: 'mapbox://styles/mapbox/satellite-streets-v12',
414
+ center: [100.5, 13.7],
415
+ zoom: 14,
416
+ });
417
+
418
+ // ควบคุมมุมมองแผนที่
419
+ mapAdapter.fitBounds(ofs.dataSource.getBoundingBox(), 50 /* padding px */);
420
+ mapAdapter.flyTo({ center: [102.84, 16.44], zoom: 15, duration: 1500 });
421
+ ```
422
+
423
+ #### ต่อกับแผนที่ที่แอปสร้างเอง (`attach`)
424
+
425
+ ถ้าโครงการมีแผนที่ของตัวเองอยู่แล้ว (เช่น Angular component สร้าง `new maplibregl.Map(...)` เอง หรือมีหลายส่วนใช้แผนที่เดียวกัน) ให้ส่ง instance เข้ามาแทนการให้ OFSObject สร้างให้:
426
+
427
+ ```typescript
428
+ const map = new maplibregl.Map({ container: el, style, center: [100.5, 13.7], zoom: 13 });
429
+
430
+ const ofs = new OFSObject({ map }); // Mapbox: เพิ่ม mapAdapter: new MapboxAdapter()
431
+ await ofs.init(); // ไม่ต้องส่ง container — รอ style พร้อมแล้วผูกให้เอง
432
+
433
+ // หรือเรียกที่ adapter ตรง ๆ
434
+ await ofs.mapAdapter.attach(map);
435
+ ```
436
+
437
+ กติกาความเป็นเจ้าของ: **ใครสร้าง คนนั้นลบ**
438
+
439
+ | | `init(container)` | `attach(map)` |
440
+ |---|---|---|
441
+ | ใครสร้างแผนที่ | OFSObject | แอปของคุณ |
442
+ | `ofs.destroy()` ทำอะไรกับแผนที่ | เรียก `map.remove()` ทิ้งทั้งตัว | ถอด source/layer ที่ OFSObject เพิ่ม (รวม `_ofs_*`) และ listener ออก **แผนที่ยังทำงานต่อ** |
443
+ | ต้องลบแผนที่เอง | ไม่ต้อง | ต้อง เรียก `map.remove()` เองเมื่อเลิกใช้ |
444
+
445
+ `attach()` รอ style ให้เองไม่ว่าจะเรียกก่อนหรือหลัง event `load` และเลเยอร์ที่มีอยู่ใน `dataSource` แล้วจะถูกวาดทันทีที่ผูกเสร็จ
446
+
447
+ ---
448
+
449
+ ### 5. AutoCAD-style Web CLI & AI Bridge
450
+
451
+ #### Web CLI Console
452
+ มี Command Engine แบบ Pipeline รองรับการพิมพ์คำสั่งเพื่อสั่งการแผนที่แบบโปรแกรมเมอร์ หรือผู้เชี่ยวชาญ CAD:
453
+
454
+ ```typescript
455
+ // สั่งรันคำสั่งเดี่ยวหรือเป็น Pipeline ต่อเนื่อง
456
+ await ofs.commandEngine.run(['poly']); // เริ่มวาด Polygon
457
+ await ofs.commandEngine.run(['buffer 20 meters']); // สร้าง Buffer 20 เมตร
458
+ await ofs.commandEngine.run(['thematic parcels crop categorical']); // เปลี่ยนสไตล์
459
+ await ofs.commandEngine.run(['zoom parcels']); // ซูมไปยังเลเยอร์
460
+ ```
461
+
462
+ #### การป้อนพิกัดแบบ AutoCAD (ระหว่างวาด หรือเมื่อคำสั่งถามจุด)
463
+ พิมพ์ได้ทันทีโดยไม่ต้องคลิกช่อง Command Line (ตัวอักษรจะถูกส่งเข้า Command Line อัตโนมัติขณะวาด)
464
+
465
+ | พิมพ์ | ความหมาย |
466
+ |---|---|
467
+ | `100.5018, 13.7563` | พิกัดสัมบูรณ์ **lng,lat** (ลองจิจูดก่อน เหมือน GeoJSON) — พิมพ์สลับจะถูกปฏิเสธพร้อมบอกค่าที่ถูก |
468
+ | `@30,40` | ห่างจากจุดล่าสุด 30 ม. ตามแกน X (ตะวันออก) และ 40 ม. ตามแกน Y (เหนือ) ของ UCS |
469
+ | `@25<60` | ห่างจากจุดล่าสุด 25 ม. ที่มุม 60° (ค่าเริ่มต้นแบบ bearing: 0°=เหนือ ตามเข็มนาฬิกา) |
470
+ | `12.5` | ระยะ 12.5 ม. ไปทางทิศที่เคอร์เซอร์ชี้ (ใช้คู่กับ ORTHO / POLAR ได้) |
471
+ | `u` | ย้อนจุดล่าสุด |
472
+ | `c` | ปิดรูป Polygon / เส้น |
473
+ | Enter (ว่าง) | จบการวาด |
474
+
475
+ ระยะและมุมทั้งหมดคำนวณบนระนาบพื้นดินจริง (Transverse Mercator เฉพาะที่) ตรวจสอบกับ GeographicLib แล้วคลาดเคลื่อนไม่เกิน 1 มม. ต่อ 1 กม. และ 0.01°
476
+
477
+
478
+ #### ตารางสรุปคำสั่งในตัว (Built-in Commands):
479
+ | คำสั่ง (Command) | คำสั่งย่อ (Alias) | พารามิเตอร์ | คำอธิบาย |
480
+ |:---|:---|:---|:---|
481
+ | `line` / `draw_line` | `l` | `[coordinates]` | วาดเส้นตรง (คลิกบนแผนที่ หรือระบุพิกัด JSON) |
482
+ | `polygon` / `draw_polygon`| `poly`, `p` | `[coordinates]` | วาดรูปแปลง Polygon |
483
+ | `point` / `draw_point` | `pt` | `[lng, lat]` | วาดจุดพิกัด Point |
484
+ | `circle` | - | `[radius]` | วาดวงกลม (คลิกจุดศูนย์กลาง + รัศมี) |
485
+ | `rectangle` | `rect` | - | วาดรูปสี่เหลี่ยมผืนผ้า |
486
+ | `guides` | `guide` | `[on\|off] [--crosshair on\|off] [--dimensions on\|off] [--segments on\|off] [--area on\|off] [--angles on\|off] [--anchor cursor\|vertex]` | เส้น guide พาดจอ, เส้นบอกระยะแบบแปลน และมุมที่หัวมุม ขณะวาด |
487
+ | `ortho` | `orthomode` | `[on\|off]` | เปิด/ปิดการล็อกแกน 90 องศา (F8) |
488
+ | `osnap` | `snap` | `[on\|off\|end\|int\|mid\|cen\|per\|nea]` | ตั้งค่า OSnap: จุดยอด, จุดตัด, กึ่งกลาง, ศูนย์กลางวงกลม, ตั้งฉาก, บนเส้น (F3 เปิด/ปิด) |
489
+ | `polar` | `polartracking` | `[on\|off\|<มุม>]` | Polar Tracking ล็อกทิศตามมุมที่กำหนด เช่น `polar 15` (F10) |
490
+ | `ucs` | - | `[world\|<มุมแกน X>]` | หมุนแกนวาดให้ตามแนวแปลง (คลิก 2 จุดตามขอบแปลง) ใช้กับ ORTHO, POLAR และ `@dx,dy` |
491
+ | `angles` | `angdir`, `units` | `[bearing\|cad]` | รูปแบบมุม: bearing (0°=เหนือ ตามเข็ม) หรือ cad (0°=ตะวันออก ทวนเข็ม) |
492
+ | `offset` | `o` | `[distance_meters [--copy\|-c]]` | ไม่ใส่ค่า: พิมพ์ระยะ (หรือ `T` = ผ่านจุด) แล้วคลิกวัตถุและคลิกฝั่ง สร้างวัตถุใหม่ได้ต่อเนื่อง / ใส่ค่า: ขนานวัตถุที่เลือก (+ ขวา/ขยายออก, - ซ้าย/ยุบเข้า) |
493
+ | `rotate` | `ro` | `[angle]` | หมุนวัตถุที่เลือกรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์มุม / คลิกทิศ / `R` (Reference มุมเดิม→มุมใหม่) |
494
+ | `scale` | `sc` | `[factor]` | ย่อ/ขยายรอบจุดฐาน ไม่ใส่ค่า: เลือกจุดฐาน แล้วพิมพ์ factor หรือ `R` (ความยาวอ้างอิง→ความยาวใหม่) |
495
+ | `mirror` | `mi` | - | สะท้อนวัตถุที่เลือกข้ามเส้นที่คลิก 2 จุด (เลือกได้ว่าลบต้นฉบับหรือไม่) |
496
+ | `explode` | - | - | แยกเส้นขอบของ Polygon ออกเป็นเส้นย่อย |
497
+ | `fillet` | `f` | `[vertex_index radius]` | ไม่ใส่ค่า: คลิก 2 เส้นเพื่อต่อมุม (รัศมี 0 = มุมแหลม, `R` ตั้งรัศมี = ส่วนโค้งวงกลมจริง) / ใส่ค่า: ลบมุมที่ vertex ของวัตถุที่เลือก |
498
+ | `trim` | `tr` | - | คลิกส่วนของเส้นที่ต้องการตัดทิ้ง ระหว่างจุดตัดกับวัตถุอื่น (Enter จบ) |
499
+ | `extend` | `ex` | - | คลิกใกล้ปลายเส้นเพื่อยืดไปชนวัตถุที่ใกล้ที่สุดตามแนวเส้น (Enter จบ) |
500
+ | `stretch` | `s` | - | ลากกรอบ แล้วย้ายเฉพาะจุดยอดที่อยู่ในกรอบ (ถ้ามี selection จะทำเฉพาะวัตถุที่เลือก) |
501
+ | `arrayrect` | `array`, `ar` | `[{"rows","columns","rowSpacing","columnSpacing"}]` | คัดลอกวัตถุที่เลือกเป็นตาราง แถว/คอลัมน์ตามแกน UCS ระยะเป็นเมตร |
502
+ | `arraypolar` | `ap` | `[{"center","count","fillAngle","rotateItems"}]` | คัดลอกวัตถุที่เลือกรอบจุดศูนย์กลาง |
503
+ | `move` | `m`, `translate` | `[dist [angle]]` | ย้ายวัตถุที่เลือก ไม่ใส่ค่า: เลือกจุดฐาน แล้วคลิกจุดที่สอง หรือพิมพ์ `@dx,dy` / `@ระยะ<มุม` |
504
+ | `buffer` | `b`, `buf` | `<dist> [units]` | สร้างพื้นที่กันชนรอบ Feature ที่เลือก |
505
+ | `area` | `measure_area` | - | คำนวณเนื้อที่ (ไร่ และ ตร.ม.) ของแปลงที่เลือก |
506
+ | `union` | `merge` | `[snap_tolerance_meters]` | รวมแปลงที่เลือกตั้งแต่ 2 แปลงขึ้นไป (รวมจุดยอดที่ห่างไม่เกิน 1 มม. หรือตามระยะที่ระบุ) |
507
+ | `split` | `cut` | `[cutter_id]` | ตัดแบ่ง Polygon ด้วยเส้นตัด (Auto-detect เส้นตัดข้ามเลเยอร์อัตโนมัติ) หรือตัดเส้น Line |
508
+ | `split_overlap` | `slice` | - | แยก 2 Polygon ที่ซ้อนทับกันออกเป็น 3 ชิ้นส่วน ($A \setminus B, A \cap B, B \setminus A$) |
509
+ | `split_line` | `splitline` | `[cutter_id]` | ตัดแบ่งเส้น LineString ออกเป็น 2 ท่อน ด้วยพิกัดจุดหรือเส้นตัด (เดิมมี alias `break` ซึ่งตอนนี้เป็นคำสั่ง BREAK แบบ AutoCAD) |
510
+ | `break` | `br` | - | คลิก 2 จุดบนเส้นเพื่อลบช่วงระหว่างจุด (คลิกจุดเดียวกัน 2 ครั้ง = แบ่งเส้นที่จุดนั้น) |
511
+ | `lengthen` | `len` | - | ปรับความยาวเส้นที่ปลายที่คลิก: DE (เพิ่ม/ลดเมตร), T (ความยาวรวม), P (เปอร์เซ็นต์) |
512
+ | `align` | `al` | - | ย้าย+หมุนวัตถุที่เลือกให้จุดต้นทาง 2 จุดตรงกับจุดปลายทาง (เลือกย่อ/ขยายให้พอดีได้) |
513
+ | `otrack` | `tracking` | `[on\|off]` | Object Snap Tracking (F11): ชี้ค้างที่จุด snap เพื่อเก็บจุด แล้วเคอร์เซอร์จะเกาะแนวแกน/มุม POLAR จากจุดนั้น |
514
+ | `join` | `connect`, `j` | `[tolerance_meters]` | เชื่อมต่อเส้น LineString 2 เส้นเข้าด้วยกัน (รักษา Direction ของเส้นแรกเป็นหลัก) |
515
+ | `reverse` | `rev` | - | กลับทิศทางของเส้น LineString (พร้อมแสดงลูกศรทิศทาง `▲` บนแผนที่ในโหมด Grips) |
516
+ | `thematic` | `classify` | `<layer> <field> [method] [classes] [palette]` | จัดสไตล์แผนที่เฉพาะเรื่อง |
517
+ | `import` | `open`, `load` | `[url] [--crs EPSG:32647] [--encoding tis-620] [--close-rings]` | นำเข้า GeoJSON / Shapefile / GeoPackage / KML / KMZ / CSV / DXF (ไม่ใส่ URL = เปิดหน้าต่างเลือกไฟล์) แล้วซูมไปที่ข้อมูล |
518
+ | `export` | `save`, `download` | `[layer[,layer2]] <geojson\|shapefile\|gpkg\|kml\|kmz\|csv\|dxf> [--crs] [--encoding tis-620] [--name] [--xy]` | ส่งออกเลเยอร์ (ค่าเริ่มต้นคือเลเยอร์ที่ active) |
519
+ | `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 ได้ |
520
+ | `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 ได้ |
521
+ | `fields` | `schema`, `field` | `[layer]` · `add <name> <type> [--default v] [--values a,b] [--expr "…"]` · `delete <name>` · `rename <old> <new>` · `set <name> [--alias …]` | ดู/แก้ schema ของฟิลด์ (undo ได้) |
522
+ | `table` | `attributes`, `attr` | `[layer] [--filter "expr"] [--fields a,b] [--sort f] [--desc] [--limit 20] [--selected]` | แสดงตาราง attribute ใน console |
523
+ | `calc` | `calculate`, `fieldcalc` | `<field> = <expression> [--layer L] [--selected] [--create] [--type number] [--skip-errors]` · `calc functions` | เครื่องคิดเลขฟิลด์ เช่น `calc ไร่ = round($area / 1600, 2) --create` |
524
+ | `select` | `query`, `selectattr` | `<expression> [--layer L] [--add\|--remove\|--intersect]` | เลือกฟีเจอร์ด้วยเงื่อนไข attribute แล้วซูมไปที่ผลลัพธ์ |
525
+ | `selectloc` | `select_location` | `<intersects\|within\|contains\|disjoint\|touches\|crosses\|within-distance> <reference layer> [--distance 50]` | เลือกตามความสัมพันธ์เชิงตำแหน่งกับอีกเลเยอร์ |
526
+ | `stats` | `statistics` | `<field> [--layer L] [--selected]` | สถิติของฟิลด์ (count, sum, mean, median, sd, distinct) |
527
+ | `grid` | `points_in_polygon` | `<boundary> --spacing 10 [--pattern square\|triangular] [--rotate 30] [--inset 2] [--into L]` | เติมจุดเป็นตารางในขอบเขต ระยะเป็นเมตรบนระนาบจริง ตัดตามขอบ |
528
+ | `network` | `connect` | `<points> --from 100.5,13.7 [--mode rows\|tree] [--angle 30] [--tolerance 1] [--into L]` | ต่อจุดเป็นโครงข่ายจากต้นทาง คืนความยาวรายเส้น |
529
+ | `geometry` | `geom`, `shape` | `[layer] [--id F] [--selected] [--limit 10] [--precision 6] [--bbox-only]` | อ่านรูปทรงกลับมาเป็น GeoJSON พร้อมพื้นที่/ความยาว/bbox (หน่วยเมตร) |
530
+ | `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` | ป้ายชื่อจากฟิลด์หรือนิพจน์ |
531
+ | `measure` | `dist`, `mea` | `[distance\|area\|angle\|feature] [lng,lat …]` | วัดระยะ/พื้นที่/มุม (คลิกบนแผนที่ หรือใส่พิกัด) พื้นที่แสดงเป็น ตร.ม. และ ไร่-งาน-วา |
532
+ | `print` | `mapexport` | `[--width 1600] [--title "…"] [--name farm-map] [--jpeg] [--no-scale] [--no-north] [--attribution "…"]` | บันทึกภาพแผนที่พร้อมมาตราส่วนและลูกศรทิศเหนือ |
533
+ | `help` | `?`, `commands` | `[command]` | รายชื่อคำสั่ง หรือพารามิเตอร์ของคำสั่งเดียว (สร้างจาก schema) |
534
+ | `macro` | `script` | `record <name>` · `stop` · `play <name> [--times 2]` · `list` · `show <name>` · `delete <name>` · `export` · `import <json>` | บันทึกชุดคำสั่งแล้วเล่นซ้ำ (เก็บใน localStorage) |
535
+ | `zoom` | `z`, `extent` | `[layerId]` | ซูมไปยังขอบเขตของเลเยอร์หรือทั้งแผนที่ |
536
+ | `undo` / `redo` | `u` | - | ย้อนกลับ หรือทำซ้ำคำสั่งล่าสุด |
537
+ | `help` | `?` | - | แสดงรายการคำสั่งทั้งหมดในระบบ |
538
+
539
+ #### การเชื่อมโยงกับ AI Agent (`OFSAiBridge`)
540
+ `OFSAiBridge` ช่วยให้คุณเชื่อมต่อแผนที่เข้ากับ LLM (Google Gemini, OpenAI GPT, Claude) ผ่าน Function Calling:
541
+
542
+ ```typescript
543
+ // 1. ดึง Tool Definitions ในรูปแบบ JSON Schema ส่งให้ LLM
544
+ const toolsSchema = ofs.aiBridge.getToolDeclarations();
545
+
546
+ // 2. เติมความรู้เฉพาะงานของแอปคุณ (ไลบรารีไม่รู้จักโดเมนของคุณ และไม่ควรรู้)
547
+ ofs.aiBridge.setInstructions(`
548
+ This project designs sprinkler irrigation layouts.
549
+ Heads overlap by 60% of spray radius; branch lines run along the longest side of the plot.
550
+ `);
551
+
552
+ // 3. ดึง System Context: สถานะแผนที่ (เลเยอร์, ขอบเขตและขนาดเป็นเมตรต่อเลเยอร์, ฟิลด์)
553
+ // + คำแนะนำของไลบรารี + คำแนะนำของแอปคุณ
554
+ const systemPrompt = ofs.aiBridge.getSystemContextPrompt();
555
+
556
+ // 4. เมื่อ LLM ส่ง Function Call กลับมา นำมาประมวลผลผ่าน executeToolCall
557
+ const feedback = await ofs.aiBridge.executeToolCall('thematic', {
558
+ layerId: 'parcels',
559
+ field: 'ndvi',
560
+ method: 'quantile',
561
+ classes: 4,
562
+ palette: 'greens',
563
+ });
564
+
565
+ console.log(feedback);
566
+ // {
567
+ // success: true,
568
+ // tool: "thematic",
569
+ // summary: "Successfully executed \"thematic\".",
570
+ // mapSnapshot: { activeLayerId: "parcels", totalLayers: 3, ... }
571
+ // }
572
+ ```
573
+
574
+ ---