shuishen
9 hours ago fbb068ec702338d609c1ca6eddbdb9f182d8f211
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
export interface Detection { class_id: number; class_name: string; confidence: number; bbox_xyxy: number[]; }
export interface GeoFeature { type?: string; properties?: Record<string, unknown>; geometry?: { type?: string; coordinates?: unknown }; }
export interface DetectionImage { file: string; annotated_file: string; width: number; height: number; detections: Detection[]; }
export interface DetectionRun { detection_count: number; processed_images: number; elapsed_seconds: number; device: string; model: string; confidence: number; input_dir: string; notes: string[]; created_at?: string; }
export interface DetectionCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; createdAt: string; run: DetectionRun; images: DetectionImage[]; }
export interface SemanticImage { file: string; width: number; height: number; mask_file: string; overlay_file: string; raster_file: string; vector_file: string; class_pixel_counts: Record<string, number>; georeferenced: boolean; vectorizer: string; elapsed_seconds: number; }
export interface SemanticRun { capability: string; classification: string; task_id?: string; task_name?: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; thresholds: Record<string, number>; input_count: number; processed_images: number; elapsed_seconds: number; images: SemanticImage[]; classes: Array<{ id: number; key: string; label: string; color: number[] }>; limitations: string[]; input_dir?: string; raw_input_dir?: string; }
export interface SemanticCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; rawInputRoot: string; createdAt: string; run: SemanticRun; }
export interface SemanticTask { id: string; name: string; status: string; selectable: boolean; input_sensor: string[]; classes: string[]; project_use?: string; required_evidence?: string[]; prohibited_claim?: string; }
export interface MeasurementImage { file: string; width: number; height: number; raster_file: string; preview_file: string; vector_file: string; csv_file: string; object_count: number; class_counts: Record<string, number>; total_area: number; total_perimeter: number; area_unit: string; length_unit: string; measurement_basis: string; crs: string | null; georeferenced: boolean; vectorizer: string; elapsed_seconds: number; }
export interface MeasurementRun { capability: string; classification: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; input_count: number; processed_images: number; elapsed_seconds: number; images: MeasurementImage[]; limitations: string[]; input_dir?: string; raw_input_dir?: string; }
export interface MeasurementCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: MeasurementRun; }
export interface PointCloudImage { file: string; original_points: number; downsampled_points: number; ground_inliers: number; elevated_points: number; elevated_point_ratio: number; elevated_footprint_count: number; mesh_triangles: number; mesh_skipped_for_large_cloud?: boolean; raster_width: number; raster_height: number; coordinate_basis: string; crs: string | null; classified_point_cloud: string; semantic_preview_point_cloud?: string; semantic_annotation_source_point_cloud?: string; semantic_preview_points?: number; semantic_classified_las?: string; semantic_raster_file?: string; semantic_preview_file?: string; semantic_vector_file?: string; semantic_summary_file?: string; semantic_footprint_count?: number; semantic_class_counts?: Record<string, number>; semantic_source_has_rgb?: boolean; semantic_method?: string; mesh_file: string | null; dsm_file: string; height_file: string; label_raster_file: string; preview_file: string; vector_file: string; summary_file: string; vectorizer: string; elapsed_seconds: number; }
export interface PhotoReconstruction { input_images: number; registered_images: number; unregistered_images: string[]; sparse_points: number; mean_reprojection_error_pixels: number; camera_count: number; coordinate_basis: string; point_cloud_file: string; preview_file: string; camera_pose_file: string; input_manifest_file: string; colmap_model_directory: string; elapsed_seconds: number; initial_min_triangulation_angle_degrees: number; }
export interface DensePhotoReconstruction { input_images: number; registered_images: number; sparse_points: number; dense_points: number; mesh_vertices: number; mesh_faces: number; preview_mesh_faces?: number; preview_mesh_face_ratio?: number; geometry_preview_faces?: number; point_color_preview_faces?: number; coordinate_basis: string; dense_point_cloud_file: string; mesh_file: string; textured_model_file: string; geometry_preview_model_file?: string; point_color_preview_model_file?: string; texture_preview_filter_report?: string; texture_file: string; elapsed_seconds: number | null; }
export interface PointCloudRun { capability: string; classification: string; created_at?: string; versions: Record<string, string>; method: string; model: string; device: string; thresholds: Record<string, number | string>; input_count?: number; processed_point_clouds?: number; elapsed_seconds: number; point_clouds?: PointCloudImage[]; photo_reconstruction?: PhotoReconstruction; dense_photo_reconstruction?: DensePhotoReconstruction; limitations: string[]; raw_input_dir?: string; }
export interface PointCloudCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: PointCloudRun; features: GeoFeature[]; }
export interface RiskScoreFeature { type: "Feature"; properties: { object_id: string; confidence: number; raw_score: number; risk_score: number; risk_level: "low" | "medium" | "high" | "critical"; score_capped: boolean; hit_rule_ids: string[]; suggestions: string[]; rule_contributions: Array<{ rule_id: string; score: number; suggestion: string }>; }; geometry: { type: string; coordinates: number[] }; }
export interface RiskSummary { object_count: number; risk_level_counts: Record<"low" | "medium" | "high" | "critical", number>; maximum_score: number; mean_score: number; hit_rule_counts: Record<string, number>; }
export interface RiskRuleRun { schema_version: number; capability: string; classification: string; created_at?: string; method: string; model: string; device: string; geoai_role: string; crs: string; zones_reprojected_to_observation_crs: boolean; parameters: { score_cap: number; levels: Record<string, number>; raster_resolution: number; rule_count: number; }; raster: { width: number; height: number; bounds: number[]; coordinate_basis: string; }; summary: RiskSummary; artifacts: { risk_raster: string; risk_preview: string; risk_vector: string; risk_scores_csv: string; summary: string; }; elapsed_seconds: number; limitations: string[]; }
export interface RiskRuleCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: RiskRuleRun; features: RiskScoreFeature[]; }
export interface AnomalyCandidate { type: "Feature"; properties: { feature_id?: number; mask_code?: number; method?: string; rule_flag?: boolean; isolation_flag?: boolean; agreement?: boolean; max_rule_score?: number; max_isolation_score?: number; reason_feature?: string; area_pixels?: number }; geometry: { type: string; coordinates: unknown }; }
export interface AnomalyImage { file: string; width: number; height: number; tile_count: number; rule_heatmap_file: string; isolation_heatmap_file: string; overlay_file: string; mask_file: string; vector_file: string; tiles_file: string; candidate_count: number; rule_anomaly_pixels: number; local_change_pixels?: number; isolation_anomaly_pixels: number; agreement_pixels: number; rule_anomaly_coverage: number; local_change_coverage?: number; isolation_anomaly_coverage: number; agreement_coverage: number; georeferenced: boolean; crs: string | null; coordinate_basis: string; vectorizer: string; elapsed_seconds: number; }
export interface AnomalyRun { capability: string; classification: string; created_at?: string; versions: Record<string, string>; method: string; model: string; device: string; reference_count: number; reference_tile_count: number; input_count: number; reference_images: Array<{ file: string; tile_count: number }>; feature_names: string[]; parameters: { tile_size: number; stride: number; threshold_quantile: number; random_state: number; spatial_mode_requested?: "auto" | "global" | "aligned"; rule_mode_selected?: "global" | "aligned" }; thresholds: { rule_score: number; isolation_score: number }; images: AnomalyImage[]; elapsed_seconds: number; limitations: string[]; }
export interface AnomalyCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; referenceRoot: string; createdAt: string; run: AnomalyRun; candidates: Record<string, AnomalyCandidate[]>; }
export interface ChangeFeature { type: "Feature"; properties: { feature_id?: number; area_pixels?: number; mean_probability?: number; max_probability?: number; bounds_pixel?: number[]; confidence?: number; class?: number }; geometry: { type: string; coordinates: unknown }; }
export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; rectangle_feature_count?: number; generic_difference?: { candidate_pixels: number; candidate_pixel_ratio: number; components_after_filter: number }; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; generic_difference_mask?: string; overlay: string; model_overlay?: string; before_processed_preview?: string; after_registered_preview?: string; vector: string; rectangle_vector?: string; rectangle_vector_wgs84?: string | null; features: string }; created_at?: string; }
export interface ChangeCase { id: string; label: string; note: string; artifactRoot: string; beforeImage: string; afterImage: string; rawAfterImage?: string; registeredBeforeImage?: string; registeredAfterImage?: string; createdAt: string; run: ChangeRun; features: ChangeFeature[]; }
export interface ChangeScanResult { id: string; label: string; threshold: number; minimumAreaPixels: number; cleanedComponents: number; changedPixels: number; changedPixelRatio: number; vectorFeatureCount: number; fullVectorFeatureCount?: number | null; rectangleFeatureCount?: number | null; overlay: string; mask: string; regions: string; vector?: string | null; rectangleVector?: string | null; rectangleVectorWgs84?: string | null; }
export interface ChangeParameterScan { id: string; label: string; note: string; artifactRoot: string; sourceRun?: string; userSubmitted?: boolean; contactSheet?: string | null; results: ChangeScanResult[]; }
export interface ChangeScanJob { id: string; status: "queued" | "running" | "completed" | "failed"; phase?: "inference" | "parameter-scan" | "vectorization" | "done" | "error"; scanId?: string; error?: string; thresholds?: number[]; minimumAreas?: number[]; }
export interface TrajectoryEvent { event_id: string; event_type: string; track_ids: string[]; start_time: string; end_time: string; duration_seconds: number; }
export interface TrajectoryCaseRun { case_id: string; input_count: number; track_count: number; event_count: number; dropped_duplicate_observations: number; elapsed_seconds: number; device: string; input: string; thresholds: Record<string, number>; created_at?: string; }
export interface TrajectorySummary { track_id: string; entity_type: string; point_count: string; distance_m: string; average_speed_mps: string; behavior_labels: string; }
export interface TrajectoryCase { id: string; label: string; note: string; artifactRoot: string; showSpatialContext: boolean; showFlyableZones: boolean; createdAt: string; run: TrajectoryCaseRun; events: TrajectoryEvent[]; summary: TrajectorySummary[]; }
interface CaseDefinition { id: string; label: string; note: string; artifactRoot: string; createdAt: string; }
interface TrajectoryDefinition extends CaseDefinition { showSpatialContext: boolean; showFlyableZones: boolean; }
interface DetectionDefinition extends CaseDefinition { inputRoot: string; }
interface SemanticDefinition extends CaseDefinition { inputRoot: string; rawInputRoot: string; }
interface MeasurementDefinition extends CaseDefinition {}
interface PointCloudDefinition extends CaseDefinition {}
interface RiskRuleDefinition extends CaseDefinition {}
interface ChangeDefinition extends CaseDefinition { beforeImage: string; afterImage: string; }
interface AnomalyDefinition extends CaseDefinition { inputRoot: string; referenceRoot: string; }
export interface UploadFilePayload { name: string; content: string; }
export interface ChangeUploadRef { uploadId: string; role: "before" | "after"; name: string; size: number; }
export interface AnomalyUploadRef { uploadId: string; role: "reference" | "input"; name: string; size: number; sha256: string; }
export interface AnomalyJob { id: string; runId: string; status: "queued" | "running" | "complete" | "failed"; createdAt: string; finishedAt?: string; error?: string; run?: AnomalyDefinition; }
export interface PhotoReconstructionUploadRef { uploadId: string; role: "photo"; name: string; size: number; sha256: string; }
export interface PointCloudUploadRef { uploadId: string; role: "pointcloud"; name: string; size: number; sha256: string; }
export interface PhotoReconstructionJob { id: string; runId: string; inputImages: number; usePositionPriors: boolean; status: "queued" | "running" | "complete" | "failed"; stage: "queued" | "sparse_sfm" | "dense_mvs" | "complete" | "failed"; createdAt: string; startedAt?: string; finishedAt?: string; error?: string; run?: PointCloudDefinition; }
export interface PointCloudAnnotationSource { id: string; runId: string; label: string; artifactRoot: string; file: string; url: string; sha256: string; pointCount: number; sourceKind: string; }
export interface PointCloudAnnotation { id: string; sourceId: string; createdAt: string; labelCount: number; classCounts: Record<string, number>; path: string; }
export interface PointCloudTrainingJob { id: string; annotationId: string; status: "queued" | "running" | "complete" | "failed"; stage: string; device: string; createdAt: string; error?: string; artifactRoot?: string; metrics?: string; model?: string; preview?: string; }
export interface PointCloudSemanticModel { id: string; label: string; artifactRoot: string; model: string; metrics: string; createdAt: string; classes: Record<string, { key: string; label: string; color: number[] }>; testF1: Record<string, number>; }
export interface PointCloudInferenceJob { id: string; runId: string; modelId: string; inputName: string; status: "queued" | "running" | "complete" | "failed"; stage: string; device: string; createdAt: string; error?: string; artifactRoot?: string; metadata?: string; preview?: string; classifiedLas?: string; classCounts?: string; summary?: string; }
 
export const artifactUrl = (path: string) => `/${path.replace(/\\/g, "/").split("/").map(encodeURIComponent).join("/")}`;
 
async function responseJson<T>(response: Response): Promise<T> {
  const payload = await response.json().catch(() => ({})) as { error?: string } & T;
  if (!response.ok) throw new Error(payload.error || `本地服务请求失败 (${response.status})`);
  return payload;
}
async function getJson<T>(path: string): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { cache: "no-store" })); }
async function getText(path: string): Promise<string> { const response = await fetch(artifactUrl(path), { cache: "no-store" }); if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`); return response.text(); }
function parseCsv(text: string): TrajectorySummary[] { const lines = text.trim().split(/\r?\n/); const headers = lines.shift()?.replace(/^\uFEFF/, "").split(",") ?? []; return lines.filter(Boolean).map((line) => { const values = line.split(","); return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])) as unknown as TrajectorySummary; }); }
 
export async function loadTrajectoryArtifacts(): Promise<Record<string, TrajectoryCase>> {
  const { runs } = await getJson<{ runs: TrajectoryDefinition[] }>("api/trajectory/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const [run, eventPayload, summary] = await Promise.all([getJson<TrajectoryCaseRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ events: TrajectoryEvent[] }>(`${definition.artifactRoot}/events.json`), getText(`${definition.artifactRoot}/trajectory_summary.csv`)]);
    return { ...definition, run, events: eventPayload.events, summary: parseCsv(summary) } satisfies TrajectoryCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadDetectionArtifacts(): Promise<Record<string, DetectionCase>> {
  const { runs } = await getJson<{ runs: DetectionDefinition[] }>("api/object-detection/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const [run, payload] = await Promise.all([getJson<DetectionRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ images: DetectionImage[] }>(`${definition.artifactRoot}/detections.json`)]);
    return { ...definition, run, images: payload.images } satisfies DetectionCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadSemanticArtifacts(): Promise<Record<string, SemanticCase>> {
  const { runs } = await getJson<{ runs: SemanticDefinition[] }>("api/semantic-mapping/runs");
  const cases = await Promise.all(runs.map(async (definition) => ({ ...definition, run: await getJson<SemanticRun>(`${definition.artifactRoot}/run_metadata.json`) } satisfies SemanticCase)));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadSemanticTasks(): Promise<SemanticTask[]> {
  return (await getJson<{ tasks: SemanticTask[] }>("api/semantic-mapping/tasks")).tasks;
}
 
export async function loadMeasurementArtifacts(): Promise<Record<string, MeasurementCase>> {
  const { runs } = await getJson<{ runs: MeasurementDefinition[] }>("api/spatial-measurement/runs");
  const cases = await Promise.all(runs.map(async (definition) => ({ ...definition, run: await getJson<MeasurementRun>(`${definition.artifactRoot}/run_metadata.json`) } satisfies MeasurementCase)));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadPointCloudArtifacts(): Promise<Record<string, PointCloudCase>> {
  const { runs } = await getJson<{ runs: PointCloudDefinition[] }>("api/3d-pointcloud/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const run = await getJson<PointCloudRun>(`${definition.artifactRoot}/run_metadata.json`);
    const features = (await Promise.all((run.point_clouds ?? []).map(async (item) => {
      const payload = await getJson<{ features?: GeoFeature[] }>(`${definition.artifactRoot}/${item.vector_file}`);
      return payload.features ?? [];
    }))).flat();
    return { ...definition, run, features } satisfies PointCloudCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadRiskRuleArtifacts(): Promise<Record<string, RiskRuleCase>> {
  const { runs } = await getJson<{ runs: RiskRuleDefinition[] }>("api/risk-rule-engine/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const run = await getJson<RiskRuleRun>(`${definition.artifactRoot}/run_metadata.json`);
    const vector = await getJson<{ features?: RiskScoreFeature[] }>(`${definition.artifactRoot}/${run.artifacts.risk_vector}`);
    return { ...definition, run, features: vector.features ?? [] } satisfies RiskRuleCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
export async function loadChangeArtifacts(): Promise<Record<string, ChangeCase>> {
  const { runs } = await getJson<{ runs: ChangeDefinition[] }>("api/change-detection/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const run = await getJson<ChangeRun>(`${definition.artifactRoot}/run_metadata.json`);
    const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.rectangle_vector || run.artifacts.vector}`);
    return { ...definition, run, features: vector.features ?? [] } satisfies ChangeCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
export async function loadChangeParameterScans(): Promise<ChangeParameterScan[]> {
  return (await getJson<{ scans: ChangeParameterScan[] }>("api/change-detection/scans")).scans;
}
 
export async function loadAnomalyArtifacts(): Promise<Record<string, AnomalyCase>> {
  const { runs } = await getJson<{ runs: AnomalyDefinition[] }>("api/anomaly-detection/runs");
  const cases = await Promise.all(runs.map(async (definition) => {
    const run = await getJson<AnomalyRun>(`${definition.artifactRoot}/run_metadata.json`);
    const entries = await Promise.all(run.images.map(async (image) => {
      const payload = await getJson<{ features?: AnomalyCandidate[] }>(`${definition.artifactRoot}/${image.vector_file}`);
      return [image.file, payload.features ?? []] as const;
    }));
    return { ...definition, run, candidates: Object.fromEntries(entries) } satisfies AnomalyCase;
  }));
  return Object.fromEntries(cases.map((item) => [item.id, item]));
}
 
async function postRun<T>(path: string, body: object): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })); }
export async function createTrajectoryRun(files: { flight: UploadFilePayload; route: UploadFilePayload; restricted: UploadFilePayload; flyable?: UploadFilePayload }) { return postRun<{ run: TrajectoryDefinition }>("api/trajectory/runs", { files }); }
export async function createDetectionRun(images: UploadFilePayload[]) { return postRun<{ run: DetectionDefinition }>("api/object-detection/runs", { images }); }
export async function createSemanticRun(images: UploadFilePayload[], taskId: string) { return postRun<{ run: SemanticDefinition }>("api/semantic-mapping/runs", { images, taskId }); }
export async function createMeasurementRun(rasters: UploadFilePayload[]) { return postRun<{ run: MeasurementDefinition }>("api/spatial-measurement/runs", { rasters }); }
export async function createPointCloudRun(pointClouds: Array<UploadFilePayload | PointCloudUploadRef>) { return postRun<{ run: PointCloudDefinition }>("api/3d-pointcloud/runs", { pointClouds }); }
export async function createDensePointCloudClassification(sourceDenseRunId: string) { return postRun<{ run: PointCloudDefinition }>("api/3d-pointcloud/runs", { sourceDenseRunId }); }
export async function uploadPointCloudFile(file: File): Promise<PointCloudUploadRef> {
  const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
  const response = await fetch(`/api/3d-pointcloud/pointcloud-uploads/${uploadId}?role=pointcloud`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
  return responseJson<PointCloudUploadRef>(response);
}
export async function uploadPhotoReconstructionFile(file: File): Promise<PhotoReconstructionUploadRef> {
  const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
  const response = await fetch(`/api/3d-pointcloud/photo-uploads/${uploadId}?role=photo`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
  return responseJson<PhotoReconstructionUploadRef>(response);
}
export async function createPhotoReconstructionRun(photos: PhotoReconstructionUploadRef[], usePositionPriors: boolean) { return postRun<{ job: PhotoReconstructionJob }>("api/3d-pointcloud/photo-reconstruction-runs", { photos, usePositionPriors }); }
export async function loadPhotoReconstructionJob(jobId: string) { return (await getJson<{ job: PhotoReconstructionJob }>(`api/3d-pointcloud/photo-reconstruction-jobs/${jobId}`)).job; }
export async function loadPointCloudAnnotationSources() { return (await getJson<{ sources: PointCloudAnnotationSource[] }>("api/3d-pointcloud/annotation-sources")).sources; }
export async function loadPointCloudAnnotations() { return (await getJson<{ annotations: PointCloudAnnotation[] }>("api/3d-pointcloud/annotations")).annotations; }
export async function createPointCloudAnnotation(sourceId: string, labels: Array<[number, number]>) { return postRun<{ annotation: PointCloudAnnotation }>("api/3d-pointcloud/annotations", { sourceId, labels }); }
export async function deletePointCloudAnnotation(annotationId: string) { return responseJson<{ deletedId: string }>(await fetch(artifactUrl(`api/3d-pointcloud/annotations/${encodeURIComponent(annotationId)}`), { method: "DELETE" })); }
export async function createPointCloudTrainingRun(annotationId: string, device: "auto" | "cpu" | "cuda" = "cpu") { return postRun<{ job: PointCloudTrainingJob }>("api/3d-pointcloud/training-runs", { annotationId, device }); }
export async function loadPointCloudTrainingJob(jobId: string) { return (await getJson<{ job: PointCloudTrainingJob }>(`api/3d-pointcloud/training-jobs/${jobId}`)).job; }
export async function loadPointCloudSemanticModels() { return (await getJson<{ models: PointCloudSemanticModel[] }>("api/3d-pointcloud/semantic-models")).models; }
export async function createPointCloudModelInference(modelId: string, pointCloud: PointCloudUploadRef) { return postRun<{ job: PointCloudInferenceJob }>("api/3d-pointcloud/model-inference-runs", { modelId, pointCloud }); }
export async function loadPointCloudModelInferenceJob(jobId: string) { return (await getJson<{ job: PointCloudInferenceJob }>(`api/3d-pointcloud/model-inference-jobs/${jobId}`)).job; }
export async function createRiskRuleRun(files: { observations: UploadFilePayload; zones: UploadFilePayload; rules: UploadFilePayload }) { return postRun<{ run: RiskRuleDefinition }>("api/risk-rule-engine/runs", { files }); }
export async function createChangeRun(files: { before: UploadFilePayload; after: UploadFilePayload }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { files, threshold, maxDimension, processingMode }); }
export async function uploadChangeFile(file: File, role: "before" | "after"): Promise<ChangeUploadRef> {
  const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
  const query = new URLSearchParams({ role });
  // HTTP header values are restricted to ISO-8859-1 by browsers. Percent-encode
  // the original name so Chinese filenames do not fail before the upload starts.
  const response = await fetch(`/api/change-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
  return responseJson<ChangeUploadRef>(response);
}
export async function createChangeRunFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { uploads, threshold, maxDimension, processingMode }); }
export async function createChangeScanFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, thresholds: number[], minimumAreas: number[], maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ job: ChangeScanJob }>("api/change-detection/scans", { uploads, thresholds, minimumAreas, maxDimension, processingMode }); }
export async function getChangeScanJob(jobId: string) { return (await getJson<{ job: ChangeScanJob }>(`api/change-detection/scan-jobs/${jobId}`)).job; }
export async function promoteChangeScan(scanId: string, resultId: string) { return postRun<{ run: ChangeDefinition }>(`api/change-detection/scans/${encodeURIComponent(scanId)}/promote`, { resultId }); }
export async function uploadAnomalyFile(file: File, role: "reference" | "input"): Promise<AnomalyUploadRef> {
  const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
  const query = new URLSearchParams({ role });
  const response = await fetch(`/api/anomaly-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
  return responseJson<AnomalyUploadRef>(response);
}
export async function createAnomalyRun(uploads: { reference: AnomalyUploadRef[]; input: AnomalyUploadRef[] }, parameters: { tileSize: number; stride: number; thresholdQuantile: number; randomState: number }) { return postRun<{ job: AnomalyJob }>("api/anomaly-detection/runs", { uploads, ...parameters }); }
export async function loadAnomalyJob(jobId: string) { return (await getJson<{ job: AnomalyJob }>(`api/anomaly-detection/jobs/${jobId}`)).job; }
export function readFileAsPayload(file: File): Promise<UploadFilePayload> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error(`无法读取 ${file.name}`)); reader.onload = () => { const value = String(reader.result ?? ""); resolve({ name: file.name, content: value.slice(value.indexOf(",") + 1) }); }; reader.readAsDataURL(file); }); }