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.
@@ -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 {};