Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Description formats:
| 063 | Infrastructure | [Analyse tool specs for phased implementation sequence](docs/ideas/063-tool-implementation-sequence.md) [E01] — dependency graph, phase groupings, and per-phase backlog items for 63 documented tools | 5 | 3 | 4 | 12 | Medium | approved |
| 085 | Feature | [Chart renderer + dataset-to-spec transformer](docs/ideas/E04-results-visualization.md) [E04] — React component with Vega-Lite (swappable); transformer converts standard result datasets to render specs | 4 | 4 | 4 | 12 | Medium | approved |
| 079 | Feature | [Implement move-track tool](docs/ideas/E03-buffer-zone-analysis-demo.md) [E03] — offset track by range/bearing with map drag support (requires #049, #062) | 4 | 4 | 4 | 12 | Medium | approved |
| 081 | Feature | [Implement point-in-zone-classifier tool](docs/ideas/E03-buffer-zone-analysis-demo.md) [E03] — classify and recolor reference points by buffer zone membership (requires #049, #078, #080) | 4 | 4 | 4 | 12 | Medium | approved |
| 081 | Feature | [Implement point-in-zone-classifier tool](specs/081-point-in-zone-classifier/spec.md) [E03] — classify and recolor reference points by buffer zone membership (requires #049, #078, #080) | 4 | 4 | 4 | 12 | Medium | specified |
| 082 | Feature | [Implement zone-histogram-generator tool](docs/ideas/E03-buffer-zone-analysis-demo.md) [E03] — outputs dataset/zone_histogram, point counts per buffer zone (requires #049, #081) | 4 | 4 | 4 | 12 | Medium | approved |
| 094 | Feature | [[E05] Implement point and rectangle drawing](docs/ideas/094-point-rectangle-drawing.md) (requires #091, #092, #093) [E05] | 4 | 4 | 4 | 12 | Medium | approved |
| 011 | Documentation | Create Jupyter notebook example demonstrating debrief-calc Python API | 4 | 4 | 4 | 12 | Low | approved |
Expand Down
10 changes: 10 additions & 0 deletions apps/vscode/src/tools/reference/classification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Reference point classification tools barrel file.
*
* Exports tool definitions and execute functions for point-in-zone classification.
*/

export {
toolDefinition as pointInZoneClassifierDefinition,
execute as pointInZoneClassifierExecute,
} from './pointInZoneClassifier';
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Point-in-Zone Classifier tool implementation.
*
* Classifies reference points by buffer zone membership using ray-casting
* point-in-polygon testing. Step 4 of the E03 buffer zone analysis chain.
*/

import type { MCPToolDefinition } from '../../../types/tool';

/** Default color for points outside all zones. */
const DEFAULT_COLOR = '#666666';
const DEFAULT_ZONE = 'none';

interface GeoJSONFeature {
type: 'Feature';
id?: string;
geometry: { type: string; coordinates: unknown };
properties: Record<string, unknown>;
}

interface PointMetadataEntry {
index: number;
name: string;
zone?: string;
color?: string;
[key: string]: unknown;
}

interface ZoneMetadata {
name: string;
detection_likelihood_pct: number;
buffer_distance_nm: number;
style: { fill_color?: string; color?: string };
}

export const toolDefinition: MCPToolDefinition = {
name: 'point-in-zone-classifier',
description:
'Classify reference points by detection zone membership. Tests each coordinate against concentric zone polygons (innermost first) and updates per-point metadata with zone name and color.',
inputSchema: {
type: 'object',
properties: {
features: { type: 'array', items: { type: 'object' } },
params: {
type: 'object',
properties: {},
},
},
},
annotations: {
'debrief:selectionRequirements': [
{ kind: 'POINT', min: 1, max: 1 },
{ kind: 'ZONE', min: 1, max: 1 },
],
'debrief:category': 'reference/classification',
'debrief:version': '1.0.0',
'debrief:outputKind': 'mutation/reference/classified_points',
},
};

/**
* Ray-casting point-in-polygon test.
*
* Casts a horizontal ray to the right from (px, py) and counts edge crossings.
* Odd count = inside, even count = outside.
*/
function pointInPolygon(px: number, py: number, ring: number[][]): boolean {
let inside = false;
const n = ring.length;
let j = n - 1;

for (let i = 0; i < n; i++) {
const xi = ring[i]![0]!;
const yi = ring[i]![1]!;
const xj = ring[j]![0]!;
const yj = ring[j]![1]!;

if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
inside = !inside;
}

j = i;
}

return inside;
}

/** Extract color from zone metadata, preferring fill_color over color. */
function getZoneColor(zoneInfo: ZoneMetadata): string {
return zoneInfo.style.fill_color ?? zoneInfo.style.color ?? DEFAULT_COLOR;
}

export function execute(
features: GeoJSONFeature[],
_params: Record<string, unknown>,
): GeoJSONFeature[] {
// Find reference and zone features
let refFeature: GeoJSONFeature | null = null;
let zoneFeature: GeoJSONFeature | null = null;

for (const feature of features) {
const props = feature.properties;
const kind = props.kind as string | undefined;
if (kind === 'POINT' && props.locationType === 'REFERENCE' && !refFeature) {
refFeature = feature;
} else if (kind === 'ZONE' && !zoneFeature) {
zoneFeature = feature;
}
}

// Validate inputs
if (!refFeature) {
throw new Error('No reference point feature found');
}
if (!zoneFeature) {
throw new Error('No zone feature found');
}

if (refFeature.geometry.type !== 'MultiPoint') {
throw new Error('Reference feature must have MultiPoint geometry');
}
if (zoneFeature.geometry.type !== 'MultiPolygon') {
throw new Error('Zone feature must have MultiPolygon geometry');
}

const coordinates = refFeature.geometry.coordinates as number[][];
const metadata = (refFeature.properties.pointMetadata ?? []) as PointMetadataEntry[];
const zonePolygons = zoneFeature.geometry.coordinates as number[][][][];
const zoneInfoList = (zoneFeature.properties.zones ?? []) as ZoneMetadata[];

if (metadata.length !== coordinates.length) {
throw new Error('pointMetadata length must match coordinates length');
}

// Handle empty coordinates
if (coordinates.length === 0) {
const classified = JSON.parse(JSON.stringify(refFeature)) as GeoJSONFeature;
classified.properties.pointMetadata = [];
classified.properties.pointColors = [];
return [classified];
}

// Classify each point
const pointColors: string[] = [];
const newMetadata: PointMetadataEntry[] = [];

for (let i = 0; i < coordinates.length; i++) {
const px = coordinates[i]![0]!; // longitude
const py = coordinates[i]![1]!; // latitude

let assignedZone = DEFAULT_ZONE;
let assignedColor = DEFAULT_COLOR;

// Test zones innermost first (index 0 = highest likelihood)
for (let z = 0; z < zonePolygons.length; z++) {
if (z < zoneInfoList.length) {
const ring = zonePolygons[z]![0]!; // outer ring of this polygon
if (pointInPolygon(px, py, ring)) {
assignedZone = zoneInfoList[z]!.name;
assignedColor = getZoneColor(zoneInfoList[z]!);
break; // innermost match wins
}
}
}

// Copy and update metadata entry (preserve existing fields)
const entry: PointMetadataEntry = { ...metadata[i]! };
entry.zone = assignedZone;
entry.color = assignedColor;
newMetadata.push(entry);
pointColors.push(assignedColor);
}

// Build classified feature (deep copy of original with updated metadata)
const classified = JSON.parse(JSON.stringify(refFeature)) as GeoJSONFeature;
classified.properties.pointMetadata = newMetadata;
classified.properties.pointColors = pointColors;

return [classified];
}
4 changes: 2 additions & 2 deletions apps/web-shell/playwright/tests/styling-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ test.describe('Styling Tools Integration', () => {
});
});

test('tools panel lists all 13 tools (2 built-in + 4 styling + 1 shape + 1 reference + 1 sensor + 1 track-manipulation + 3 analysis)', async ({ page }) => {
test('tools panel lists all 14 tools (2 built-in + 4 styling + 1 shape + 2 reference + 1 sensor + 1 track-manipulation + 3 analysis)', async ({ page }) => {
const toolItems = page.locator('.debrief-tools-panel__item');
await expect(toolItems).toHaveCount(13);
await expect(toolItems).toHaveCount(14);
});

test('styling tools are listed by name', async ({ page }) => {
Expand Down
12 changes: 12 additions & 0 deletions apps/web-shell/src/services/toolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ import {
execute as executeAreaSummary,
} from '../tools/region/analysis/areaSummary';

import {
toolDefinition as pointInZoneClassifierDef,
execute as executePointInZoneClassifier,
} from '../../../vscode/src/tools/reference/classification/pointInZoneClassifier';

// Re-export types for consumers
export type { MCPToolDefinition, MCPToolResponse, MCPContentItem, DebriefAnnotations };

Expand Down Expand Up @@ -219,6 +224,13 @@ const toolRegistry: Map<string, ToolRegistryEntry> = new Map([
execute: executeAreaSummary,
},
],
[
pointInZoneClassifierDef.name,
{
definition: pointInZoneClassifierDef,
execute: executePointInZoneClassifier,
},
],
]);

/**
Expand Down
4 changes: 3 additions & 1 deletion services/calc/debrief_calc/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
- generate-reference-points: Generate grid/scatter reference points in a bounding box
- generate-courses-speeds: Derive course and speed from consecutive positions
- buffer-zone-generator: Generate detection likelihood buffer zones around a track
- point-in-zone-classifier: Classify reference points by buffer zone membership
"""

# Import tools to trigger registration via @tool decorator
from debrief_calc.tools import area_summary, range_bearing, track_stats
from debrief_calc.tools.reference import generation
from debrief_calc.tools.reference import classification, generation
from debrief_calc.tools.sensor import detection
from debrief_calc.tools.shape import manipulation
from debrief_calc.tools.track import styling
Expand All @@ -30,6 +31,7 @@
"area_summary",
"styling",
"manipulation",
"classification",
"generation",
"detection",
]
4 changes: 2 additions & 2 deletions services/calc/debrief_calc/tools/reference/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
Tools for generating and managing reference point sets.
"""

from debrief_calc.tools.reference import generation
from debrief_calc.tools.reference import classification, generation

__all__ = ["generation"]
__all__ = ["classification", "generation"]
Loading
Loading