Skip to content
Merged
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
9 changes: 4 additions & 5 deletions apps/vscode/src/commands/executeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { LogService, InputFeatureState, ResultIdRegistry } from '@debrief/s
import type { LogPanelViewProvider } from '../views/logPanelView';
import type { ToolParameter } from '../types/tool';
import type { DebriefFeature } from '@debrief/components';
import { propsRecord, parseJsonSafe } from '../utils/featureProps';

/**
* Known parameter type → values map.
Expand Down Expand Up @@ -153,12 +152,12 @@ export function createExecuteToolCommand(
);
if (preToolFeatures.length > 0) {
preToolInputState = preToolFeatures.map((f: DebriefFeature) => {
const props = propsRecord(f);
const { provenance: _p, ...restProps } = props;
// Deep-copy geometry and properties (minus provenance) for input state snapshot.
const { provenance: _p, ...restProps } = structuredClone(f.properties);
const state: InputFeatureState = {
featureId: String(f.id),
geometry: parseJsonSafe(JSON.stringify(f.geometry)),
properties: parseJsonSafe(JSON.stringify(restProps)) as InputFeatureState['properties'],
geometry: structuredClone(f.geometry),
properties: restProps,
};
return state;
});
Expand Down
23 changes: 16 additions & 7 deletions apps/vscode/src/commands/openPlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@ import type { ActivityPanelViewProvider } from '../views/activityPanelView';
import type { LogPanelViewProvider } from '../views/logPanelView';
import { MapPanel } from '../webview/mapPanel';
import { isTrackFeature, isReferenceLocation } from '@debrief/components';
import type { DebriefFeature } from '@debrief/components';
import { parseStacUri, buildStacUri } from '../types/stac';
import type { SafeFeature, SafeFeatureCollection, SafeGeometry } from '@debrief/utils';
import { propsRecord } from '../utils/featureProps';

/** Extract a display name from a DebriefFeature. Uses type-specific property names. */
function featureDisplayName(f: DebriefFeature): string {
if (isTrackFeature(f)) {
return f.properties.platform_name ?? f.properties.platform_id ?? String(f.id);
}
if (isReferenceLocation(f)) {
return f.properties.name ?? String(f.id);
}
// Annotation types — use 'label' or fall back to id
const props = f.properties as { label?: string; name?: string; text?: string };
return props.label ?? props.name ?? props.text ?? String(f.id);
}

/** Adapt a SafeFeature to a loosely-typed record for session-state log service. */
function safeFeatureToRecord(f: SafeFeature): Record<string, unknown> {
Expand Down Expand Up @@ -410,9 +423,7 @@ export function createOpenPlotCommand(
// for features created/removed during replay
const updatedNames: Record<string, string> = {};
for (const f of updatedData.features) {
const props = propsRecord(f);
const name = String(props.name ?? props.title ?? f.id);
updatedNames[String(f.id)] = name;
updatedNames[String(f.id)] = featureDisplayName(f);
}
logPanelProvider.setFeatureNames(updatedNames);
}
Expand Down Expand Up @@ -460,9 +471,7 @@ export function createOpenPlotCommand(
if (logPanelProvider) {
const featureNames: Record<string, string> = {};
for (const f of plotData.features) {
const props = propsRecord(f);
const name = String(props.name ?? props.title ?? f.id);
featureNames[String(f.id)] = name;
featureNames[String(f.id)] = featureDisplayName(f);
}
logPanelProvider.setFeatureNames(featureNames);
}
Expand Down
10 changes: 6 additions & 4 deletions apps/vscode/src/providers/outlineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
*/

import * as vscode from 'vscode';
import type { Track, ReferenceLocation } from '../types/plot';
// T018: Track renamed to TrackViewModel; T019: update import
// T019: ReferenceLocation (UI type) renamed to ReferenceLocationViewModel
import type { TrackViewModel, ReferenceLocationViewModel } from '../types/plot';

export class OutlineProvider implements vscode.DocumentSymbolProvider {
private tracks: Track[] = [];
private locations: ReferenceLocation[] = [];
private tracks: TrackViewModel[] = [];
private locations: ReferenceLocationViewModel[] = [];

/**
* Update data for outline
*/
setData(tracks: Track[], locations: ReferenceLocation[]): void {
setData(tracks: TrackViewModel[], locations: ReferenceLocationViewModel[]): void {
this.tracks = tracks;
this.locations = locations;
}
Expand Down
6 changes: 2 additions & 4 deletions apps/vscode/src/services/calcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
import { adaptMCPToolsForMatching } from './mcpToolAdapter';
import type { MapPanel } from '../webview/mapPanel';
import type { DebriefFeature } from '@debrief/schemas';
import { propsRecord } from '../utils/featureProps';

const execFileAsync = promisify(execFile);

Expand Down Expand Up @@ -668,13 +667,12 @@ print(json.dumps(tools))
for (const id of featureIds) {
const feature: DebriefFeature | undefined = allFeatures.find((f: DebriefFeature) => String(f.id) === id);
if (feature !== undefined) {
const props = propsRecord(feature);
resolved.push({
resolved.push({
type: 'Feature',
id: feature.id,
geometry: feature.geometry,
properties: {
...props,
...structuredClone(feature.properties),
id: feature.id,
},
});
Expand Down
7 changes: 3 additions & 4 deletions apps/vscode/src/services/stacService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1209,13 +1209,12 @@ export class StacService {
const feature = featureMap.get(featureId);
if (!feature) { continue; }

// Ensure properties exists
// Ensure properties exists — SafeFeature.properties is Record<string, unknown> | null
if (!feature.properties) {
// eslint-disable-next-line no-restricted-syntax -- mutating SafeFeature at parse boundary
(feature as unknown as Record<string, unknown>).properties = {};
feature.properties = {};
}

const props = feature.properties!;
const props = feature.properties;
// Normalise provenance to array (FR-006: handle legacy single-object format)
let existing = props['provenance'];
if (existing === undefined || existing === null) {
Expand Down
21 changes: 14 additions & 7 deletions apps/vscode/src/tools/shape/manipulation/enlargeShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type { DebriefFeature } from '@debrief/schemas';
import { propsRecord } from '../../../utils/featureProps';
import { isAnnotationFeature } from '@debrief/schemas';
import type { MCPToolDefinition } from '../../../types/tool';

export interface EnlargeShapeParams {
Expand Down Expand Up @@ -143,8 +143,11 @@ export function execute(
const modified: DebriefFeature[] = [];

for (const feature of features) {
const props = propsRecord(feature);
const kind = props['kind'] as string;
if (!isAnnotationFeature(feature)) {
continue;
}

const kind = feature.properties.kind;

if (!ANNOTATION_KINDS.has(kind)) {
continue;
Expand Down Expand Up @@ -172,17 +175,21 @@ export function execute(
geometry.coordinates = polyCoords.map((ring) =>
scaleCoordsList(ring, scalingOrigin, scaleFactor),
);
if (kind === 'CIRCLE' && props['center'] !== undefined) {
props['center'] = scaleCoordinate(props['center'] as number[], scalingOrigin, scaleFactor);
if (kind === 'CIRCLE') {
const circleProps = feature.properties as { center?: number[] };
if (circleProps.center !== undefined) {
circleProps.center = scaleCoordinate(circleProps.center, scalingOrigin, scaleFactor);
}
}
} else if (kind === 'LINE') {
geometry.coordinates = scaleCoordsList(coords as number[][], scalingOrigin, scaleFactor);
} else if (kind === 'TEXT') {
geometry.coordinates = scaleCoordinate(coords as number[], scalingOrigin, scaleFactor);
} else if (kind === 'VECTOR') {
geometry.coordinates = scaleCoordsList(coords as number[][], scalingOrigin, scaleFactor);
if (props['origin'] !== undefined) {
props['origin'] = scaleCoordinate(props['origin'] as number[], scalingOrigin, scaleFactor);
const vectorProps = feature.properties as { origin?: number[] };
if (vectorProps.origin !== undefined) {
vectorProps.origin = scaleCoordinate(vectorProps.origin, scalingOrigin, scaleFactor);
}
}

Expand Down
23 changes: 15 additions & 8 deletions apps/vscode/src/tools/shape/manipulation/moveShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { DebriefFeature } from '@debrief/schemas';
import { propsRecord } from '../../../utils/featureProps';
import { isAnnotationFeature } from '@debrief/schemas';
import type { MCPToolDefinition } from '../../../types/tool';

export interface MoveShapeParams {
Expand Down Expand Up @@ -110,14 +110,17 @@ export function execute(features: DebriefFeature[], params: MoveShapeParams): De

// Zero distance is a no-op
if (distanceKm === 0) {
return features.filter((f) => ANNOTATION_KINDS.has(propsRecord(f)['kind'] as string));
return features.filter((f) => isAnnotationFeature(f) && ANNOTATION_KINDS.has(f.properties.kind));
}

const modified: DebriefFeature[] = [];

for (const feature of features) {
const props = propsRecord(feature);
const kind = props['kind'] as string;
if (!isAnnotationFeature(feature)) {
continue;
}

const kind = feature.properties.kind;

if (!ANNOTATION_KINDS.has(kind)) {
continue;
Expand All @@ -135,17 +138,21 @@ export function execute(features: DebriefFeature[], params: MoveShapeParams): De
geometry.coordinates = polyCoords.map((ring) =>
translateCoordsList(ring, direction, distanceKm),
);
if (kind === 'CIRCLE' && props['center'] !== undefined && props['center'] !== null) {
props['center'] = translateCoordinate(props['center'] as number[], direction, distanceKm);
if (kind === 'CIRCLE') {
const circleProps = feature.properties as { center?: number[] };
if (circleProps.center !== undefined && circleProps.center !== null) {
circleProps.center = translateCoordinate(circleProps.center, direction, distanceKm);
}
}
} else if (kind === 'LINE') {
geometry.coordinates = translateCoordsList(coords as number[][], direction, distanceKm);
} else if (kind === 'TEXT') {
geometry.coordinates = translateCoordinate(coords as number[], direction, distanceKm);
} else if (kind === 'VECTOR') {
geometry.coordinates = translateCoordsList(coords as number[][], direction, distanceKm);
if (props['origin'] !== undefined && props['origin'] !== null) {
props['origin'] = translateCoordinate(props['origin'] as number[], direction, distanceKm);
const vectorProps = feature.properties as { origin?: number[] };
if (vectorProps.origin !== undefined && vectorProps.origin !== null) {
vectorProps.origin = translateCoordinate(vectorProps.origin, direction, distanceKm);
}
}

Expand Down
42 changes: 12 additions & 30 deletions apps/vscode/src/tools/track/styling/applySymbolStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { TrackFeature } from '@debrief/schemas';
import { propsRecord } from '../../../utils/featureProps';
import { isTrackFeature } from '@debrief/schemas';
import type { MCPToolDefinition } from '../../../types/tool';

const VALID_SYMBOLS = ['circle', 'square', 'diamond', 'triangle', 'cross'] as const;
Expand Down Expand Up @@ -41,23 +41,6 @@ export const toolDefinition: MCPToolDefinition = {
},
};

interface PointStyle {
shape?: string;
radius?: number;
fill?: boolean;
fill_color?: string;
fill_opacity?: number;
stroke?: boolean;
color?: string;
weight?: number;
opacity?: number;
}

interface TrackStyle {
line?: { color?: string };
point?: PointStyle;
}

export function execute(
features: TrackFeature[],
params: ApplySymbolStyleParams,
Expand All @@ -76,13 +59,13 @@ export function execute(
const modified: TrackFeature[] = [];

for (const feature of features) {
const props = propsRecord(feature);
if (props['kind'] !== 'TRACK') {
if (!isTrackFeature(feature)) {
continue;
}

const style = (props['style'] as TrackStyle) ?? {};
const point: PointStyle = style.point ?? {
// Defensively handle missing style — real features from disk may lack it
const style = feature.properties.style ?? { line: {} };
const point = style.point ?? {
shape: 'square', radius: 4, fill: true,
fill_color: '#3388ff', fill_opacity: 0.8,
stroke: true, color: '#ffffff', weight: 1, opacity: 1.0,
Expand All @@ -93,22 +76,21 @@ export function execute(
point.radius = radius;
}

const lineStyle = style.line as { color?: string } | undefined;
if (fill_color !== undefined) {
point.fill_color = fill_color;
} else if (!point.fill_color && lineStyle?.color) {
point.fill_color = lineStyle.color;
} else if (!point.fill_color && style.line?.color) {
point.fill_color = style.line.color;
}

style.point = point;
props['style'] = style;
feature.properties.style = style;

// Update default_position_style so the PositionSymbolsLayer renderer
// shows the chosen symbol shape on the map.
const dps = (props['default_position_style'] ?? {}) as { [k: string]: unknown };
dps['symbol'] = symbol;
dps['show_symbol'] = true;
props['default_position_style'] = dps;
const dps = feature.properties.default_position_style ?? { show_symbol: false, show_label: false };
dps.symbol = symbol;
dps.show_symbol = true;
feature.properties.default_position_style = dps;

modified.push(feature);
}
Expand Down
21 changes: 8 additions & 13 deletions apps/vscode/src/tools/track/styling/labelInterval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { TrackFeature } from '@debrief/schemas';
import { propsRecord } from '../../../utils/featureProps';
import { isTrackFeature } from '@debrief/schemas';
import type { MCPToolDefinition } from '../../../types/tool';

export interface LabelIntervalParams {
Expand All @@ -29,13 +29,6 @@ export const toolDefinition: MCPToolDefinition = {
},
};

interface DefaultPositionStyle {
show_symbol?: boolean;
symbol?: string;
show_label?: boolean;
label_interval?: string;
}

export function execute(
features: TrackFeature[],
params: LabelIntervalParams,
Expand All @@ -45,17 +38,19 @@ export function execute(
const modified: TrackFeature[] = [];

for (const feature of features) {
const props = propsRecord(feature);
if (props['kind'] !== 'TRACK') {
if (!isTrackFeature(feature)) {
continue;
}

const dps: DefaultPositionStyle = (props['default_position_style'] as DefaultPositionStyle) ?? {
// Defensively handle missing default_position_style.
// label_interval is stored on default_position_style alongside show_label
// (runtime extension beyond the PositionStyle schema type).
const dps = feature.properties.default_position_style ?? {
show_symbol: true, symbol: 'circle', show_label: false,
};
dps.show_label = true;
dps.label_interval = interval;
props['default_position_style'] = dps;
Object.assign(dps, { label_interval: interval });
feature.properties.default_position_style = dps;

modified.push(feature);
}
Expand Down
Loading
Loading