kaax-mcp 0.1.6 → 0.1.8
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/dist/gis-reports.d.ts +61 -0
- package/dist/gis-reports.js +293 -0
- package/dist/gis-reports.js.map +1 -0
- package/dist/gis-rows.d.ts +122 -0
- package/dist/gis-rows.js +348 -0
- package/dist/gis-rows.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/tools.js +206 -0
- package/dist/tools.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports — turn one or several layers into formats the user can actually
|
|
3
|
+
* share with their team: Excel spreadsheets for the office and a single
|
|
4
|
+
* markdown summary for the agent / chat.
|
|
5
|
+
*
|
|
6
|
+
* Excel: every layer becomes a worksheet. Each feature is one row,
|
|
7
|
+
* columns are the union of every property name seen, plus columns for
|
|
8
|
+
* geometry type / WKT (Polygon, LineString, etc.) or lon/lat (Point).
|
|
9
|
+
* A first "Summary" sheet gives the agronomic totals (counts, area in
|
|
10
|
+
* hectares, length in kilometres, distinct row_ids if present).
|
|
11
|
+
*
|
|
12
|
+
* Markdown: a print-ready human-readable digest with one section per
|
|
13
|
+
* layer + a final comparison table. Stays small enough to paste into
|
|
14
|
+
* Slack or a PDF, and the agent reading it can extract numbers without
|
|
15
|
+
* a second tool call.
|
|
16
|
+
*/
|
|
17
|
+
import { type AnyFeatureCollection } from "./gis-io.js";
|
|
18
|
+
export interface ExcelExportOptions {
|
|
19
|
+
/** Limit the number of features written — defaults to no limit. */
|
|
20
|
+
limit?: number;
|
|
21
|
+
/**
|
|
22
|
+
* If true, add a `geometry_wkt` column with WKT for non-Point geometries.
|
|
23
|
+
* Off by default because WKT for big polygons blows up cell size.
|
|
24
|
+
*/
|
|
25
|
+
includeWkt?: boolean;
|
|
26
|
+
/** Override the sheet name; default 'Features'. */
|
|
27
|
+
sheetName?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write one or several FeatureCollections to a single XLSX file. The
|
|
31
|
+
* first sheet is `Summary`; the following sheets carry the features of
|
|
32
|
+
* each layer, one feature per row.
|
|
33
|
+
*/
|
|
34
|
+
export declare function exportLayersToExcel(layers: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
fc: AnyFeatureCollection;
|
|
37
|
+
}>, outputPath: string, options?: ExcelExportOptions): Promise<{
|
|
38
|
+
outputPath: string;
|
|
39
|
+
sheets: number;
|
|
40
|
+
totalRows: number;
|
|
41
|
+
}>;
|
|
42
|
+
export interface SummaryReportOptions {
|
|
43
|
+
/** Optional title for the report. */
|
|
44
|
+
title?: string;
|
|
45
|
+
/** Add a Kaax footer link at the bottom. */
|
|
46
|
+
footer?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a markdown digest summarising every input layer. Lands on
|
|
50
|
+
* disk as a single .md file; small enough to paste into Slack / a PR
|
|
51
|
+
* description or attach to a PDF for the field crew.
|
|
52
|
+
*/
|
|
53
|
+
export declare function generateMarkdownSummary(inputs: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
path: string;
|
|
56
|
+
}>, outputPath: string, options?: SummaryReportOptions): Promise<{
|
|
57
|
+
outputPath: string;
|
|
58
|
+
layers: number;
|
|
59
|
+
}>;
|
|
60
|
+
/** Build a friendly default name from a path. */
|
|
61
|
+
export declare function defaultLayerName(path: string): string;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports — turn one or several layers into formats the user can actually
|
|
3
|
+
* share with their team: Excel spreadsheets for the office and a single
|
|
4
|
+
* markdown summary for the agent / chat.
|
|
5
|
+
*
|
|
6
|
+
* Excel: every layer becomes a worksheet. Each feature is one row,
|
|
7
|
+
* columns are the union of every property name seen, plus columns for
|
|
8
|
+
* geometry type / WKT (Polygon, LineString, etc.) or lon/lat (Point).
|
|
9
|
+
* A first "Summary" sheet gives the agronomic totals (counts, area in
|
|
10
|
+
* hectares, length in kilometres, distinct row_ids if present).
|
|
11
|
+
*
|
|
12
|
+
* Markdown: a print-ready human-readable digest with one section per
|
|
13
|
+
* layer + a final comparison table. Stays small enough to paste into
|
|
14
|
+
* Slack or a PDF, and the agent reading it can extract numbers without
|
|
15
|
+
* a second tool call.
|
|
16
|
+
*/
|
|
17
|
+
import ExcelJS from "exceljs";
|
|
18
|
+
import { writeFile } from "fs/promises";
|
|
19
|
+
import { basename } from "path";
|
|
20
|
+
import * as turf from "@turf/turf";
|
|
21
|
+
import { readAny } from "./gis-io.js";
|
|
22
|
+
import { summariseLayer } from "./gis-ops.js";
|
|
23
|
+
/**
|
|
24
|
+
* Write one or several FeatureCollections to a single XLSX file. The
|
|
25
|
+
* first sheet is `Summary`; the following sheets carry the features of
|
|
26
|
+
* each layer, one feature per row.
|
|
27
|
+
*/
|
|
28
|
+
export async function exportLayersToExcel(layers, outputPath, options = {}) {
|
|
29
|
+
const workbook = new ExcelJS.Workbook();
|
|
30
|
+
workbook.creator = "kaax-mcp";
|
|
31
|
+
workbook.created = new Date();
|
|
32
|
+
// Summary sheet — populate after we count, so leave a placeholder.
|
|
33
|
+
// exceljs accepts Partial<Column> shapes at runtime but the TS type
|
|
34
|
+
// for `worksheet.columns` is the full `Column[]`. Cast at the boundary
|
|
35
|
+
// rather than fill in every property we don't use.
|
|
36
|
+
const summary = workbook.addWorksheet("Summary");
|
|
37
|
+
summary.columns = [
|
|
38
|
+
{ header: "Layer", key: "layer", width: 28 },
|
|
39
|
+
{ header: "Features", key: "count", width: 11 },
|
|
40
|
+
{ header: "Geometry types", key: "kinds", width: 24 },
|
|
41
|
+
{ header: "Area (ha)", key: "areaHa", width: 12 },
|
|
42
|
+
{ header: "Length (km)", key: "lengthKm", width: 13 },
|
|
43
|
+
{ header: "Distinct row_ids", key: "rows", width: 16 },
|
|
44
|
+
];
|
|
45
|
+
summary.getRow(1).font = { bold: true };
|
|
46
|
+
let totalRows = 0;
|
|
47
|
+
for (const { name, fc } of layers) {
|
|
48
|
+
const sheet = workbook.addWorksheet(safeSheetName(name));
|
|
49
|
+
writeFeatureSheet(sheet, fc, options);
|
|
50
|
+
const stats = layerStats(fc);
|
|
51
|
+
summary.addRow({
|
|
52
|
+
layer: name,
|
|
53
|
+
count: stats.count,
|
|
54
|
+
kinds: Object.entries(stats.byGeometry)
|
|
55
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
56
|
+
.join(", "),
|
|
57
|
+
areaHa: stats.totalAreaHa ? Number(stats.totalAreaHa.toFixed(3)) : null,
|
|
58
|
+
lengthKm: stats.totalLengthM
|
|
59
|
+
? Number((stats.totalLengthM / 1000).toFixed(3))
|
|
60
|
+
: null,
|
|
61
|
+
rows: stats.distinctRows ?? null,
|
|
62
|
+
});
|
|
63
|
+
totalRows += stats.count;
|
|
64
|
+
}
|
|
65
|
+
// Tidy the summary header style.
|
|
66
|
+
summary.getRow(1).alignment = { vertical: "middle", horizontal: "left" };
|
|
67
|
+
await workbook.xlsx.writeFile(outputPath);
|
|
68
|
+
return { outputPath, sheets: layers.length, totalRows };
|
|
69
|
+
}
|
|
70
|
+
function writeFeatureSheet(sheet, fc, options) {
|
|
71
|
+
const limit = options.limit ?? fc.features.length;
|
|
72
|
+
const features = fc.features.slice(0, limit);
|
|
73
|
+
// Collect the union of property names across all features (sorted).
|
|
74
|
+
const propNames = new Set();
|
|
75
|
+
for (const f of features) {
|
|
76
|
+
for (const k of Object.keys(f.properties ?? {}))
|
|
77
|
+
propNames.add(k);
|
|
78
|
+
}
|
|
79
|
+
const sortedProps = [...propNames].sort();
|
|
80
|
+
// Columns: properties + geometry summary. Same Partial-vs-full Column
|
|
81
|
+
// shrug as the Summary sheet — cast at the boundary.
|
|
82
|
+
const columns = [
|
|
83
|
+
{ header: "feature_index", key: "feature_index", width: 14 },
|
|
84
|
+
{ header: "geometry_type", key: "geometry_type", width: 16 },
|
|
85
|
+
{ header: "lon", key: "lon", width: 12 },
|
|
86
|
+
{ header: "lat", key: "lat", width: 12 },
|
|
87
|
+
...sortedProps.map((p) => ({ header: p, key: `p_${p}`, width: 18 })),
|
|
88
|
+
];
|
|
89
|
+
if (options.includeWkt) {
|
|
90
|
+
columns.push({ header: "geometry_wkt", key: "wkt", width: 80 });
|
|
91
|
+
}
|
|
92
|
+
sheet.columns = columns;
|
|
93
|
+
sheet.getRow(1).font = { bold: true };
|
|
94
|
+
features.forEach((f, idx) => {
|
|
95
|
+
const row = {
|
|
96
|
+
feature_index: idx,
|
|
97
|
+
geometry_type: f.geometry?.type ?? "(none)",
|
|
98
|
+
};
|
|
99
|
+
const centroid = geometryRepresentativePoint(f);
|
|
100
|
+
if (centroid) {
|
|
101
|
+
row.lon = centroid[0];
|
|
102
|
+
row.lat = centroid[1];
|
|
103
|
+
}
|
|
104
|
+
for (const p of sortedProps) {
|
|
105
|
+
const v = f.properties?.[p];
|
|
106
|
+
// Excel can store strings / numbers / booleans / Date. Coerce
|
|
107
|
+
// everything else to JSON so the cell never carries [Object].
|
|
108
|
+
row[`p_${p}`] =
|
|
109
|
+
v === null || v === undefined
|
|
110
|
+
? null
|
|
111
|
+
: typeof v === "object"
|
|
112
|
+
? JSON.stringify(v)
|
|
113
|
+
: v;
|
|
114
|
+
}
|
|
115
|
+
if (options.includeWkt && f.geometry) {
|
|
116
|
+
row.wkt = geometryToWKT(f.geometry);
|
|
117
|
+
}
|
|
118
|
+
sheet.addRow(row);
|
|
119
|
+
});
|
|
120
|
+
// Freeze the header row.
|
|
121
|
+
sheet.views = [{ state: "frozen", ySplit: 1 }];
|
|
122
|
+
}
|
|
123
|
+
/** Produce a small representative point (lon, lat) for a feature. */
|
|
124
|
+
function geometryRepresentativePoint(f) {
|
|
125
|
+
if (!f.geometry)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
if (f.geometry.type === "Point") {
|
|
129
|
+
const c = f.geometry.coordinates;
|
|
130
|
+
return [c[0], c[1]];
|
|
131
|
+
}
|
|
132
|
+
const ctr = turf.pointOnFeature(f);
|
|
133
|
+
const c = ctr.geometry.coordinates;
|
|
134
|
+
return [c[0], c[1]];
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Minimal WKT serialiser — enough for QGIS-style import / paste-into-
|
|
142
|
+
* PostGIS. Skips Z values; Kaax data is 2-D.
|
|
143
|
+
*/
|
|
144
|
+
function geometryToWKT(g) {
|
|
145
|
+
const ring = (coords) => `(${coords.map((c) => `${c[0]} ${c[1]}`).join(", ")})`;
|
|
146
|
+
const poly = (rings) => `(${rings.map((r) => ring(r)).join(", ")})`;
|
|
147
|
+
switch (g.type) {
|
|
148
|
+
case "Point": {
|
|
149
|
+
const c = g.coordinates;
|
|
150
|
+
return `POINT (${c[0]} ${c[1]})`;
|
|
151
|
+
}
|
|
152
|
+
case "MultiPoint":
|
|
153
|
+
return `MULTIPOINT (${g.coordinates
|
|
154
|
+
.map((c) => `${c[0]} ${c[1]}`)
|
|
155
|
+
.join(", ")})`;
|
|
156
|
+
case "LineString":
|
|
157
|
+
return `LINESTRING ${ring(g.coordinates)}`;
|
|
158
|
+
case "MultiLineString":
|
|
159
|
+
return `MULTILINESTRING (${g.coordinates
|
|
160
|
+
.map((ls) => ring(ls))
|
|
161
|
+
.join(", ")})`;
|
|
162
|
+
case "Polygon":
|
|
163
|
+
return `POLYGON ${poly(g.coordinates)}`;
|
|
164
|
+
case "MultiPolygon":
|
|
165
|
+
return `MULTIPOLYGON (${g.coordinates
|
|
166
|
+
.map((p) => poly(p))
|
|
167
|
+
.join(", ")})`;
|
|
168
|
+
default:
|
|
169
|
+
return `(${g.type})`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function safeSheetName(raw) {
|
|
173
|
+
// Excel sheet names: max 31 chars, no : \ / ? * [ ]
|
|
174
|
+
let s = raw.replace(/[:\\/?*[\]]/g, "_").slice(0, 31);
|
|
175
|
+
if (!s)
|
|
176
|
+
s = "Sheet";
|
|
177
|
+
return s;
|
|
178
|
+
}
|
|
179
|
+
function layerStats(fc) {
|
|
180
|
+
const base = summariseLayer(fc);
|
|
181
|
+
const rowIds = new Set();
|
|
182
|
+
let hasRowId = false;
|
|
183
|
+
for (const f of fc.features) {
|
|
184
|
+
const rid = f.properties?.row_id;
|
|
185
|
+
if (typeof rid === "number") {
|
|
186
|
+
hasRowId = true;
|
|
187
|
+
rowIds.add(rid);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let bbox;
|
|
191
|
+
try {
|
|
192
|
+
const b = turf.bbox(fc);
|
|
193
|
+
bbox = [b[0], b[1], b[2], b[3]];
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* skip — empty or bad geometry */
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
...base,
|
|
200
|
+
...(hasRowId ? { distinctRows: rowIds.size } : {}),
|
|
201
|
+
...(bbox ? { bbox } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Generate a markdown digest summarising every input layer. Lands on
|
|
206
|
+
* disk as a single .md file; small enough to paste into Slack / a PR
|
|
207
|
+
* description or attach to a PDF for the field crew.
|
|
208
|
+
*/
|
|
209
|
+
export async function generateMarkdownSummary(inputs, outputPath, options = {}) {
|
|
210
|
+
const sections = [];
|
|
211
|
+
sections.push(`# ${options.title ?? "Kaax MCP — Layer Report"}`);
|
|
212
|
+
sections.push("");
|
|
213
|
+
sections.push(`Generated ${new Date().toISOString()}`);
|
|
214
|
+
sections.push("");
|
|
215
|
+
sections.push(`Layers analysed: **${inputs.length}**`);
|
|
216
|
+
sections.push("");
|
|
217
|
+
// Per-layer detail
|
|
218
|
+
const allStats = [];
|
|
219
|
+
for (const inp of inputs) {
|
|
220
|
+
sections.push(`## ${inp.name}`);
|
|
221
|
+
sections.push("");
|
|
222
|
+
sections.push(`Source: \`${inp.path}\``);
|
|
223
|
+
try {
|
|
224
|
+
const fc = await readAny(inp.path);
|
|
225
|
+
const s = layerStats(fc);
|
|
226
|
+
allStats.push({ name: inp.name, stats: s, path: inp.path });
|
|
227
|
+
sections.push("");
|
|
228
|
+
sections.push(`- **Features**: ${s.count}`);
|
|
229
|
+
sections.push(`- **Geometry types**: ${Object.entries(s.byGeometry)
|
|
230
|
+
.map(([k, v]) => `${k} × ${v}`)
|
|
231
|
+
.join(", ") || "—"}`);
|
|
232
|
+
if (s.totalAreaHa !== undefined) {
|
|
233
|
+
sections.push(`- **Total area**: ${s.totalAreaHa.toFixed(2)} ha`);
|
|
234
|
+
}
|
|
235
|
+
if (s.totalLengthM !== undefined) {
|
|
236
|
+
sections.push(`- **Total length**: ${(s.totalLengthM / 1000).toFixed(2)} km`);
|
|
237
|
+
}
|
|
238
|
+
if (s.distinctRows !== undefined) {
|
|
239
|
+
sections.push(`- **Rows (distinct row_id)**: ${s.distinctRows}`);
|
|
240
|
+
}
|
|
241
|
+
if (s.bbox) {
|
|
242
|
+
sections.push(`- **Bounding box**: ${s.bbox
|
|
243
|
+
.map((n) => n.toFixed(5))
|
|
244
|
+
.join(", ")} (minLon, minLat, maxLon, maxLat)`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
sections.push("");
|
|
249
|
+
sections.push(`> ⚠️ Failed to read this layer: ${err.message}`);
|
|
250
|
+
}
|
|
251
|
+
sections.push("");
|
|
252
|
+
}
|
|
253
|
+
// Comparison table if there are 2+ layers.
|
|
254
|
+
if (allStats.length >= 2) {
|
|
255
|
+
sections.push("## Comparison");
|
|
256
|
+
sections.push("");
|
|
257
|
+
sections.push("| Layer | Features | Area (ha) | Length (km) | Rows |");
|
|
258
|
+
sections.push("|---|---:|---:|---:|---:|");
|
|
259
|
+
for (const s of allStats) {
|
|
260
|
+
const cells = [
|
|
261
|
+
s.name,
|
|
262
|
+
String(s.stats.count),
|
|
263
|
+
s.stats.totalAreaHa !== undefined
|
|
264
|
+
? s.stats.totalAreaHa.toFixed(2)
|
|
265
|
+
: "—",
|
|
266
|
+
s.stats.totalLengthM !== undefined
|
|
267
|
+
? (s.stats.totalLengthM / 1000).toFixed(2)
|
|
268
|
+
: "—",
|
|
269
|
+
s.stats.distinctRows !== undefined
|
|
270
|
+
? String(s.stats.distinctRows)
|
|
271
|
+
: "—",
|
|
272
|
+
];
|
|
273
|
+
sections.push(`| ${cells.join(" | ")} |`);
|
|
274
|
+
}
|
|
275
|
+
sections.push("");
|
|
276
|
+
}
|
|
277
|
+
if (options.footer !== false) {
|
|
278
|
+
sections.push("---");
|
|
279
|
+
sections.push("");
|
|
280
|
+
sections.push("Generated by [kaax-mcp](https://www.npmjs.com/package/kaax-mcp).");
|
|
281
|
+
}
|
|
282
|
+
const md = sections.join("\n");
|
|
283
|
+
await writeFile(outputPath, md, "utf8");
|
|
284
|
+
return { outputPath, layers: inputs.length };
|
|
285
|
+
}
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
287
|
+
// Helpers for tools.ts
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
289
|
+
/** Build a friendly default name from a path. */
|
|
290
|
+
export function defaultLayerName(path) {
|
|
291
|
+
return basename(path, /\.[^.]+$/.exec(path)?.[0] ?? "");
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=gis-reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gis-reports.js","sourceRoot":"","sources":["../src/gis-reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row reconstruction from point clouds — the agricultural-specific GIS
|
|
3
|
+
* pipeline. Given a SHP of plant detections, build the surco geometry:
|
|
4
|
+
*
|
|
5
|
+
* 1. Detect the dominant orientation of the field (histogram of pairwise
|
|
6
|
+
* bearings between nearby points — surcos are colinear, so the
|
|
7
|
+
* bearing distribution has a clear modal value).
|
|
8
|
+
* 2. Cluster every point into a row id, by projecting onto the axis
|
|
9
|
+
* perpendicular to the orientation and 1-D DBSCAN-ing the projection.
|
|
10
|
+
* 3. Generate one or more LineStrings per row, with **gap detection**:
|
|
11
|
+
* consecutive points within N × median(spacing) get connected (these
|
|
12
|
+
* are gaps caused by missing / dead plants — depopulation). Gaps
|
|
13
|
+
* larger than that are presumed to be physical streets / roads
|
|
14
|
+
* cutting the surco, so the line is split into two segments and the
|
|
15
|
+
* gap can be emitted as a `street` feature for the user to inspect.
|
|
16
|
+
*
|
|
17
|
+
* Everything runs in pure JS — turf for haversine distance + bearing, a
|
|
18
|
+
* small local equirectangular projection for the 1-D math (constant
|
|
19
|
+
* scaling factor against the centroid latitude — accurate enough for any
|
|
20
|
+
* single field, much faster than running every cluster step through
|
|
21
|
+
* great-circle geometry).
|
|
22
|
+
*
|
|
23
|
+
* Limits:
|
|
24
|
+
* - Pairwise bearing computation is O(n²). For n > 8 000 points we
|
|
25
|
+
* sample randomly to keep wall-clock under ~3 s on a laptop. The
|
|
26
|
+
* final clustering uses every point.
|
|
27
|
+
* - Geometry assumes lon/lat (EPSG:4326). For larger geographies the
|
|
28
|
+
* equirectangular approximation breaks down; in practice farm fields
|
|
29
|
+
* are < 5 km wide so the error is sub-meter.
|
|
30
|
+
*/
|
|
31
|
+
import type { AnyFeatureCollection } from "./gis-io.js";
|
|
32
|
+
export interface OrientationResult {
|
|
33
|
+
/** Dominant row angle in degrees, wrapped to [0, 180). */
|
|
34
|
+
angleDeg: number;
|
|
35
|
+
/** Same value in radians. */
|
|
36
|
+
angleRad: number;
|
|
37
|
+
/**
|
|
38
|
+
* Fraction of the considered point-pairs whose bearing fell in the
|
|
39
|
+
* dominant bin. Useful to decide whether the field has a clear
|
|
40
|
+
* orientation (>0.4 is strong, 0.2 is weak / spotty).
|
|
41
|
+
*/
|
|
42
|
+
confidence: number;
|
|
43
|
+
/** Total pairs considered after distance filter and sampling. */
|
|
44
|
+
totalPairsConsidered: number;
|
|
45
|
+
/** Width of each histogram bin in degrees (5° by default). */
|
|
46
|
+
binSizeDeg: number;
|
|
47
|
+
}
|
|
48
|
+
interface DetectOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Maximum distance in meters between two points to count them as a
|
|
51
|
+
* "candidate row neighbour". Default 5 m — most agricultural rows are
|
|
52
|
+
* 0.5–3 m apart with detections every 0.3–1.5 m along the row, so 5 m
|
|
53
|
+
* captures intra-row pairs but not unrelated points across the field.
|
|
54
|
+
*/
|
|
55
|
+
maxNeighborDistanceM?: number;
|
|
56
|
+
/** Histogram bin size in degrees. Default 5°. */
|
|
57
|
+
binSizeDeg?: number;
|
|
58
|
+
/**
|
|
59
|
+
* If point count exceeds this, sample down before computing pairwise
|
|
60
|
+
* bearings. The dominant angle is statistically stable from a few
|
|
61
|
+
* thousand pairs — no need to compute all of O(n²).
|
|
62
|
+
*/
|
|
63
|
+
maxPointsForPairwise?: number;
|
|
64
|
+
}
|
|
65
|
+
export declare function detectRowOrientation(fc: AnyFeatureCollection, options?: DetectOptions): OrientationResult;
|
|
66
|
+
export interface ClusterOptions {
|
|
67
|
+
/** Pre-computed orientation. If absent, we detect it. */
|
|
68
|
+
angleRad?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Expected row spacing in meters. If absent, we estimate it as the
|
|
71
|
+
* median of nearest perpendicular-distance gaps.
|
|
72
|
+
*/
|
|
73
|
+
rowSpacingM?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Multiplier applied to rowSpacingM to define the "max gap between
|
|
76
|
+
* adjacent clusters". 0.55 by default — anything more than ~half a row
|
|
77
|
+
* spacing is treated as a fresh cluster.
|
|
78
|
+
*/
|
|
79
|
+
clusterGapFraction?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Return a copy of `fc` where every Point feature carries a new `row_id`
|
|
83
|
+
* property (1-indexed integer). Non-point features are skipped.
|
|
84
|
+
*/
|
|
85
|
+
export declare function clusterPointsByRow(fc: AnyFeatureCollection, options?: ClusterOptions): AnyFeatureCollection;
|
|
86
|
+
export interface RowLinesOptions {
|
|
87
|
+
/**
|
|
88
|
+
* Gaps larger than this multiplier × median(spacing within a row) are
|
|
89
|
+
* treated as streets / roads and split the row. Default: 3 — empirical
|
|
90
|
+
* sweet spot from sugarcane and pineapple datasets.
|
|
91
|
+
*/
|
|
92
|
+
gapMultiplier?: number;
|
|
93
|
+
/** Also emit a separate FeatureCollection of detected street gaps. */
|
|
94
|
+
includeStreets?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Optional pre-computed orientation; otherwise we detect it from the
|
|
97
|
+
* same points. Used to project for spacing computation.
|
|
98
|
+
*/
|
|
99
|
+
angleRad?: number;
|
|
100
|
+
}
|
|
101
|
+
export interface RowLinesResult {
|
|
102
|
+
/** LineString features, one per contiguous row segment. */
|
|
103
|
+
lines: AnyFeatureCollection;
|
|
104
|
+
/** Optional point-pair features marking detected street gaps. */
|
|
105
|
+
streets?: AnyFeatureCollection;
|
|
106
|
+
/** Stats — useful to render as a tool output summary. */
|
|
107
|
+
stats: {
|
|
108
|
+
rows: number;
|
|
109
|
+
segments: number;
|
|
110
|
+
streetGapsDetected: number;
|
|
111
|
+
totalLengthM: number;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build LineStrings from a clustered point set. Expects each point to
|
|
116
|
+
* carry a `row_id` property (output of `clusterPointsByRow`). Each row
|
|
117
|
+
* produces one or more LineStrings depending on whether any large gap
|
|
118
|
+
* was detected inside it — those gaps are presumed to be streets, not
|
|
119
|
+
* depopulation, and split the row at that point.
|
|
120
|
+
*/
|
|
121
|
+
export declare function generateRowLines(fc: AnyFeatureCollection, options?: RowLinesOptions): RowLinesResult;
|
|
122
|
+
export {};
|