Skip to content

Commit 34e4abb

Browse files
committed
feat: implement confidence score slider
1 parent 7b4693c commit 34e4abb

File tree

4 files changed

+277
-6
lines changed

4 files changed

+277
-6
lines changed

src/components/OsmlMenu.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,43 @@
360360
background: rgba(255, 255, 255, 0.22);
361361
}
362362

363+
/* ==============================
364+
Confidence Filter
365+
============================== */
366+
.op-confidence-filter {
367+
margin: 2px 4px 10px;
368+
padding: 10px;
369+
border-radius: 9px;
370+
background: rgba(255, 255, 255, 0.03);
371+
border: 1px solid rgba(255, 255, 255, 0.06);
372+
}
373+
374+
.op-confidence-filter-header {
375+
display: flex;
376+
align-items: center;
377+
justify-content: space-between;
378+
margin-bottom: 8px;
379+
}
380+
381+
.op-confidence-filter-title {
382+
font-size: 10px;
383+
font-weight: 600;
384+
letter-spacing: 0.4px;
385+
text-transform: uppercase;
386+
color: rgba(255, 255, 255, 0.42);
387+
}
388+
389+
.op-confidence-filter-value {
390+
font-size: 11px;
391+
font-weight: 600;
392+
color: rgba(255, 255, 255, 0.78);
393+
}
394+
395+
.op-confidence-filter-range {
396+
width: 100%;
397+
accent-color: #3b82f6;
398+
}
399+
363400
/* ==============================
364401
Layer Group
365402
============================== */

src/components/OsmlMenu.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ const OsmlMenu: React.FC<OsmlMenuProps> = ({
3434
onFeatureClick
3535
}) => {
3636
const cesium = useContext(CesiumContext);
37-
const { resources, removeResource, toggleVisibility, zoomTo, clearAll } = useResources();
37+
const {
38+
resources,
39+
confidenceThreshold,
40+
setConfidenceThreshold,
41+
removeResource,
42+
toggleVisibility,
43+
zoomTo,
44+
clearAll
45+
} = useResources();
3846

3947
const [isOpen, setIsOpen] = useState(false);
4048
const [showImageRequestModal, setShowImageRequestModal] = useState(false);
@@ -84,6 +92,8 @@ const OsmlMenu: React.FC<OsmlMenuProps> = ({
8492
const renderResourceItem = (resource: LoadedResource) => {
8593
const isFC = resource.type === "feature-collection";
8694
const fc = isFC ? (resource as FeatureCollectionResource) : null;
95+
const filteredFeatureCount = fc?.filteredFeatureCount ?? fc?.featureCount ?? 0;
96+
const isFiltered = filteredFeatureCount !== (fc?.featureCount ?? 0);
8797

8898
return (
8999
<div
@@ -120,7 +130,9 @@ const OsmlMenu: React.FC<OsmlMenuProps> = ({
120130
)}
121131
{fc && (
122132
<span className="op-layer-count">
123-
{fc.featureCount.toLocaleString()} feature{fc.featureCount !== 1 ? "s" : ""}
133+
{isFiltered
134+
? `${filteredFeatureCount.toLocaleString()} / ${fc.featureCount.toLocaleString()} shown`
135+
: `${fc.featureCount.toLocaleString()} feature${fc.featureCount !== 1 ? "s" : ""}`}
124136
</span>
125137
)}
126138
<span className="op-layer-time">{formatTime(resource.loadedAt)}</span>
@@ -269,6 +281,24 @@ const OsmlMenu: React.FC<OsmlMenuProps> = ({
269281
renderEmptyLayers()
270282
) : (
271283
<>
284+
{featureCollections.length > 0 && (
285+
<div className="op-confidence-filter">
286+
<div className="op-confidence-filter-header">
287+
<span className="op-confidence-filter-title">Minimum confidence</span>
288+
<span className="op-confidence-filter-value">{confidenceThreshold.toFixed(2)}</span>
289+
</div>
290+
<input
291+
className="op-confidence-filter-range"
292+
type="range"
293+
min={0}
294+
max={1}
295+
step={0.01}
296+
value={confidenceThreshold}
297+
onChange={(e) => setConfidenceThreshold(Number(e.target.value))}
298+
/>
299+
</div>
300+
)}
301+
272302
{/* Feature Collections */}
273303
{featureCollections.length > 0 && (
274304
<div className="op-layer-group">

src/context/ResourceContext.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { GeoJsonDataSource, ImageryLayer, Viewer } from "cesium";
99
import React, { createContext, useCallback, useContext, useState } from "react";
1010

1111
import { CAMERA_FLY_DURATION_SECONDS } from "@/config";
12+
import {
13+
applyConfidenceFilter,
14+
clampConfidenceThreshold
15+
} from "@/utils/featureConfidence";
1216
import { logger } from "@/utils/logger";
1317

1418
/** A loaded GeoJSON feature collection displayed on the Cesium globe. */
@@ -23,6 +27,8 @@ export interface FeatureCollectionResource {
2327
visible: boolean;
2428
loadedAt: Date;
2529
dataSource: GeoJsonDataSource;
30+
filteredFeatureCount?: number;
31+
confidenceThreshold?: number;
2632
}
2733

2834
/** A loaded imagery layer displayed on the Cesium globe. */
@@ -42,6 +48,8 @@ export type LoadedResource = FeatureCollectionResource | ImageryResource;
4248

4349
interface ResourceContextValue {
4450
resources: LoadedResource[];
51+
confidenceThreshold: number;
52+
setConfidenceThreshold: (threshold: number) => void;
4553
addResource: (resource: LoadedResource) => void;
4654
removeResource: (id: string, viewer: Viewer) => void;
4755
toggleVisibility: (id: string, viewer: Viewer) => void;
@@ -53,9 +61,50 @@ const ResourceContext = createContext<ResourceContextValue | undefined>(undefine
5361

5462
export const ResourceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
5563
const [resources, setResources] = useState<LoadedResource[]>([]);
64+
const [confidenceThreshold, setConfidenceThresholdState] = useState(0);
5665

5766
const addResource = useCallback((resource: LoadedResource) => {
58-
setResources((prev) => [...prev, resource]);
67+
setResources((prev) => {
68+
if (resource.type !== "feature-collection") {
69+
return [...prev, resource];
70+
}
71+
72+
const featureResource = resource as FeatureCollectionResource;
73+
const filteredFeatureCount = applyConfidenceFilter(
74+
featureResource.dataSource,
75+
confidenceThreshold,
76+
featureResource.visible
77+
);
78+
return [
79+
...prev,
80+
{
81+
...featureResource,
82+
filteredFeatureCount,
83+
confidenceThreshold
84+
}
85+
];
86+
});
87+
}, [confidenceThreshold]);
88+
89+
const setConfidenceThreshold = useCallback((threshold: number) => {
90+
const clamped = clampConfidenceThreshold(threshold);
91+
setConfidenceThresholdState(clamped);
92+
setResources((prev) =>
93+
prev.map((resource) => {
94+
if (resource.type !== "feature-collection") return resource;
95+
const featureResource = resource as FeatureCollectionResource;
96+
const filteredFeatureCount = applyConfidenceFilter(
97+
featureResource.dataSource,
98+
clamped,
99+
featureResource.visible
100+
);
101+
return {
102+
...featureResource,
103+
confidenceThreshold: clamped,
104+
filteredFeatureCount
105+
};
106+
})
107+
);
59108
}, []);
60109

61110
const removeResource = useCallback((id: string, viewer: Viewer) => {
@@ -93,7 +142,17 @@ export const ResourceProvider: React.FC<{ children: React.ReactNode }> = ({ chil
93142
try {
94143
if (r.type === "feature-collection") {
95144
const fc = r as FeatureCollectionResource;
96-
fc.dataSource.show = newVisible;
145+
const filteredFeatureCount = applyConfidenceFilter(
146+
fc.dataSource,
147+
confidenceThreshold,
148+
newVisible
149+
);
150+
return {
151+
...fc,
152+
visible: newVisible,
153+
confidenceThreshold,
154+
filteredFeatureCount
155+
};
97156
} else if (r.type === "imagery") {
98157
const img = r as ImageryResource;
99158
img.imageryLayer.show = newVisible;
@@ -105,7 +164,7 @@ export const ResourceProvider: React.FC<{ children: React.ReactNode }> = ({ chil
105164
return { ...r, visible: newVisible };
106165
})
107166
);
108-
}, []);
167+
}, [confidenceThreshold]);
109168

110169
const zoomTo = useCallback((id: string, viewer: Viewer) => {
111170
// Use the state setter pattern to avoid stale closure over resources
@@ -169,7 +228,16 @@ export const ResourceProvider: React.FC<{ children: React.ReactNode }> = ({ chil
169228

170229
return (
171230
<ResourceContext.Provider
172-
value={{ resources, addResource, removeResource, toggleVisibility, zoomTo, clearAll }}
231+
value={{
232+
resources,
233+
confidenceThreshold,
234+
setConfidenceThreshold,
235+
addResource,
236+
removeResource,
237+
toggleVisibility,
238+
zoomTo,
239+
clearAll
240+
}}
173241
>
174242
{children}
175243
</ResourceContext.Provider>

src/utils/featureConfidence.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2023-2026 Amazon.com, Inc. or its affiliates.
2+
3+
/**
4+
* Confidence parsing and filtering helpers for loaded GeoJSON detections.
5+
*/
6+
7+
import * as Cesium from "cesium";
8+
import { GeoJsonDataSource } from "cesium";
9+
10+
const CONFIDENCE_KEYS = ["confidence", "conf", "probability", "prob"];
11+
const SCORE_KEYS = ["score"];
12+
13+
/** Clamps a confidence threshold to the supported 0..1 range. */
14+
export function clampConfidenceThreshold(value: number): number {
15+
if (!Number.isFinite(value)) return 0;
16+
if (value < 0) return 0;
17+
if (value > 1) return 1;
18+
return value;
19+
}
20+
21+
/** Normalizes a confidence value to a 0..1 score when possible. */
22+
function normalizeConfidence(value: number): number | undefined {
23+
if (!Number.isFinite(value) || value < 0) return undefined;
24+
if (value <= 1) return value;
25+
if (value <= 100) return value / 100;
26+
return undefined;
27+
}
28+
29+
/** Returns true when key indicates confidence-like values. */
30+
function isConfidenceKey(key: string): boolean {
31+
const lower = key.toLowerCase();
32+
return CONFIDENCE_KEYS.some((candidate) => lower.includes(candidate));
33+
}
34+
35+
/** Returns true when key indicates score-like values. */
36+
function isScoreKey(key: string): boolean {
37+
const lower = key.toLowerCase();
38+
return SCORE_KEYS.some((candidate) => lower.includes(candidate));
39+
}
40+
41+
/** Walks nested feature properties and collects confidence candidates. */
42+
function collectConfidenceCandidates(
43+
value: unknown,
44+
parentKey: string,
45+
confidenceCandidates: number[],
46+
scoreCandidates: number[],
47+
depth: number = 0
48+
): void {
49+
if (depth > 4 || value === null || value === undefined) return;
50+
51+
if (typeof value === "number") {
52+
const normalized = normalizeConfidence(value);
53+
if (normalized === undefined) return;
54+
if (isConfidenceKey(parentKey)) confidenceCandidates.push(normalized);
55+
if (isScoreKey(parentKey)) scoreCandidates.push(normalized);
56+
return;
57+
}
58+
59+
if (Array.isArray(value)) {
60+
for (const item of value) {
61+
collectConfidenceCandidates(
62+
item,
63+
parentKey,
64+
confidenceCandidates,
65+
scoreCandidates,
66+
depth + 1
67+
);
68+
}
69+
return;
70+
}
71+
72+
if (typeof value === "object") {
73+
for (const [key, nestedValue] of Object.entries(
74+
value as Record<string, unknown>
75+
)) {
76+
collectConfidenceCandidates(
77+
nestedValue,
78+
key,
79+
confidenceCandidates,
80+
scoreCandidates,
81+
depth + 1
82+
);
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Extracts a normalized confidence value from GeoJSON feature properties.
89+
* Prefers explicit confidence keys over generic score keys.
90+
*/
91+
export function extractFeatureConfidence(
92+
properties: Record<string, unknown>
93+
): number | undefined {
94+
const confidenceCandidates: number[] = [];
95+
const scoreCandidates: number[] = [];
96+
97+
collectConfidenceCandidates(
98+
properties,
99+
"",
100+
confidenceCandidates,
101+
scoreCandidates
102+
);
103+
104+
if (confidenceCandidates.length > 0) return Math.max(...confidenceCandidates);
105+
if (scoreCandidates.length > 0) return Math.max(...scoreCandidates);
106+
return undefined;
107+
}
108+
109+
/**
110+
* Applies a confidence threshold by toggling each feature entity visibility.
111+
* Returns how many features match the threshold.
112+
*/
113+
export function applyConfidenceFilter(
114+
dataSource: GeoJsonDataSource,
115+
threshold: number,
116+
baseVisible: boolean
117+
): number {
118+
const clampedThreshold = clampConfidenceThreshold(threshold);
119+
const now = Cesium.JulianDate.now();
120+
let matchingCount = 0;
121+
122+
for (const entity of dataSource.entities.values) {
123+
const properties = entity.properties?.getValue(now) as
124+
| Record<string, unknown>
125+
| undefined;
126+
const confidence = properties
127+
? extractFeatureConfidence(properties)
128+
: undefined;
129+
130+
const passes = confidence === undefined || confidence >= clampedThreshold;
131+
if (passes) matchingCount += 1;
132+
entity.show = baseVisible && passes;
133+
}
134+
135+
return matchingCount;
136+
}

0 commit comments

Comments
 (0)