mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-11 10:43:46 +08:00
range
This commit is contained in:
@@ -118,7 +118,7 @@ export const AppShell = {
|
||||
</button>
|
||||
<span class="gx-appbar__home" role="button" tabindex="0"
|
||||
:aria-label="tr('Galaxy home')" @click="goHome" @keydown.enter="goHome" @keydown.space.prevent="goHome">
|
||||
<span class="gx-appbar__title">{{ tr("Galaxy") }}</span>
|
||||
<span class="gx-appbar__title">Galaxy</span>
|
||||
</span>
|
||||
<div class="gx-searchwrap">
|
||||
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" :placeholder="tr('Search toggles...')"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GalaxyConfirm } from "./GalaxyModal.js"
|
||||
import { GxNotice } from "./GxNotice.js"
|
||||
|
||||
const MAX_ROUTES = 250
|
||||
const MAX_SEGMENTS = 5
|
||||
|
||||
export const LateralTuningPanel = {
|
||||
name: "LateralTuningPanel",
|
||||
@@ -20,6 +21,7 @@ export const LateralTuningPanel = {
|
||||
laneCentering: false,
|
||||
routes: [],
|
||||
selectedRoutes: [],
|
||||
segmentRanges: {},
|
||||
report: null,
|
||||
reportLoading: false,
|
||||
loadedReportId: "",
|
||||
@@ -29,6 +31,7 @@ export const LateralTuningPanel = {
|
||||
feedbackNotes: "",
|
||||
pending: null,
|
||||
pendingName: "",
|
||||
maxSegments: MAX_SEGMENTS,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -102,7 +105,10 @@ export const LateralTuningPanel = {
|
||||
return !!(rep && rep.car && rep.car.controlPath === "angle")
|
||||
},
|
||||
canAnalyze() {
|
||||
return this.selectedRoutes.length > 0 && !this.isOnroad && !this.laneCentering
|
||||
return this.selectedRoutes.length > 0 && this.selectedSegmentCount > 0 && this.selectedSegmentCount <= MAX_SEGMENTS && !this.isOnroad && !this.laneCentering
|
||||
},
|
||||
selectedSegmentCount() {
|
||||
return this.selectedRoutes.reduce((total, routeName) => total + this.routeSelectedSegmentCount(routeName), 0)
|
||||
},
|
||||
canApplyTrial() {
|
||||
const trial = this.activeTrial
|
||||
@@ -147,6 +153,32 @@ export const LateralTuningPanel = {
|
||||
const dur = min >= 60 ? `${Math.floor(min / 60)}h ${min % 60}m` : `~${min} min`
|
||||
return `${seg} segment${seg === 1 ? "" : "s"} (${dur})`
|
||||
},
|
||||
routeSegmentBounds(routeName) {
|
||||
const route = this.routes.find((item) => item && item.name === routeName) || {}
|
||||
const count = Math.max(0, Math.round(this.num(route.segmentCount)))
|
||||
const first = Math.max(0, Math.round(this.num(route.firstSegmentNum)))
|
||||
return { first, last: first + Math.max(0, count - 1), count }
|
||||
},
|
||||
routeSelectedSegmentCount(routeName) {
|
||||
const bounds = this.routeSegmentBounds(routeName)
|
||||
if (!bounds.count) return 0
|
||||
const selected = this.segmentRanges[routeName] || {}
|
||||
const rawStart = String(selected.start ?? "").trim()
|
||||
const rawEnd = String(selected.end ?? "").trim()
|
||||
const start = Math.min(bounds.last, Math.max(bounds.first, rawStart === "" ? bounds.first : this.num(rawStart, bounds.first)))
|
||||
const end = Math.max(bounds.first, Math.min(bounds.last, rawEnd === "" ? bounds.last : this.num(rawEnd, bounds.last)))
|
||||
return end >= start ? end - start + 1 : 0
|
||||
},
|
||||
setSegmentRange(routeName, key, value) {
|
||||
const cleaned = String(value ?? "").replace(/[^\d]/g, "")
|
||||
this.segmentRanges = {
|
||||
...this.segmentRanges,
|
||||
[routeName]: {
|
||||
...(this.segmentRanges[routeName] || {}),
|
||||
[key]: cleaned,
|
||||
},
|
||||
}
|
||||
},
|
||||
syncFeedback(report) {
|
||||
const fb = (report && report.feedback) || {}
|
||||
this.feedbackAccepted = Array.isArray(fb.acceptedDimensions) ? [...fb.acceptedDimensions] : []
|
||||
@@ -307,14 +339,65 @@ export const LateralTuningPanel = {
|
||||
},
|
||||
toggleRoute(name) {
|
||||
const set = new Set(this.selectedRoutes)
|
||||
if (set.has(name)) set.delete(name); else set.add(name)
|
||||
const ranges = { ...this.segmentRanges }
|
||||
if (set.has(name)) {
|
||||
set.delete(name)
|
||||
delete ranges[name]
|
||||
} else {
|
||||
const remaining = MAX_SEGMENTS - this.selectedSegmentCount
|
||||
if (remaining <= 0) {
|
||||
showSnackbar(`FLM is limited to ${MAX_SEGMENTS} segments at a time.`, "error")
|
||||
return
|
||||
}
|
||||
const bounds = this.routeSegmentBounds(name)
|
||||
if (!bounds.count) return
|
||||
set.add(name)
|
||||
if (bounds.count > remaining) {
|
||||
ranges[name] = { start: String(bounds.first), end: String(bounds.first + remaining - 1) }
|
||||
}
|
||||
}
|
||||
this.segmentRanges = ranges
|
||||
this.selectedRoutes = [...set]
|
||||
},
|
||||
clearSelection() { this.selectedRoutes = [] },
|
||||
selectFirstSegments() {
|
||||
const selected = []
|
||||
const ranges = {}
|
||||
let remaining = MAX_SEGMENTS
|
||||
for (const route of this.routes) {
|
||||
if (remaining <= 0) break
|
||||
const bounds = this.routeSegmentBounds(route.name)
|
||||
if (!bounds.count) continue
|
||||
const take = Math.min(bounds.count, remaining)
|
||||
selected.push(route.name)
|
||||
if (take < bounds.count) {
|
||||
ranges[route.name] = { start: String(bounds.first), end: String(bounds.first + take - 1) }
|
||||
}
|
||||
remaining -= take
|
||||
}
|
||||
this.selectedRoutes = selected
|
||||
this.segmentRanges = ranges
|
||||
},
|
||||
clearSelection() {
|
||||
this.selectedRoutes = []
|
||||
this.segmentRanges = {}
|
||||
},
|
||||
selectedSegmentRanges() {
|
||||
const ranges = {}
|
||||
for (const routeName of this.selectedRoutes) {
|
||||
const selected = this.segmentRanges[routeName] || {}
|
||||
const start = String(selected.start ?? "").trim()
|
||||
const end = String(selected.end ?? "").trim()
|
||||
if (start || end) ranges[routeName] = { start: start || null, end: end || null }
|
||||
}
|
||||
return ranges
|
||||
},
|
||||
async analyze() {
|
||||
if (!this.canAnalyze || this.busy) return
|
||||
const ok = await this.runWith(() => api.flmAnalyze(this.selectedRoutes, {}), "FLM analysis started.")
|
||||
if (ok && this.selectedRoutes.length) this.selectedRoutes = []
|
||||
const ok = await this.runWith(() => api.flmAnalyze(this.selectedRoutes, this.selectedSegmentRanges()), "FLM analysis started.")
|
||||
if (ok && this.selectedRoutes.length) {
|
||||
this.selectedRoutes = []
|
||||
this.segmentRanges = {}
|
||||
}
|
||||
},
|
||||
async stopAnalyze() {
|
||||
await this.runWith(() => api.flmStopAnalyze(), "FLM analysis stopped.")
|
||||
@@ -520,24 +603,33 @@ export const LateralTuningPanel = {
|
||||
<span class="gx-section__title">Local Routes</span>
|
||||
</div>
|
||||
<div style="padding: var(--sp-4);">
|
||||
<p style="color: var(--text-muted); line-height:1.6; margin:0 0 var(--sp-2);">Pick up to 8 routes to analyze. Whole routes are used.</p>
|
||||
<p style="color: var(--text-muted); line-height:1.6; margin:0 0 var(--sp-2);">Pick routes and segment ranges to analyze. A run is limited to 5 segments total.</p>
|
||||
<div v-if="loadingRoutes" class="gx-loading">Loading local routes...</div>
|
||||
<div v-else-if="!routes.length" class="gx-empty">No local routes found.</div>
|
||||
<div v-else>
|
||||
<label class="gx-chip" style="cursor:pointer;" :style="'user-select:none;'">
|
||||
<input type="checkbox" :checked="selectedRoutes.length === routes.length" style="margin-right:6px;" @change="selectedRoutes = (selectedRoutes.length === routes.length) ? [] : routes.map(r => r.name)" />
|
||||
Select all
|
||||
</label>
|
||||
<span class="gx-chip" :style="selectedSegmentCount > maxSegments ? 'background:var(--error);color:var(--on-error);' : ''">
|
||||
{{ selectedSegmentCount }}/{{ maxSegments }} segments selected
|
||||
</span>
|
||||
<button type="button" class="gx-btn gx-btn--text" style="font-size:var(--fs-xs);" @click="selectFirstSegments">Select first {{ maxSegments }}</button>
|
||||
<button type="button" class="gx-btn gx-btn--text" style="font-size:var(--fs-xs);" @click="clearSelection">Clear</button>
|
||||
<div v-for="route in routes" :key="route.name" style="border-top:1px solid var(--glass-border); padding: var(--sp-2) 0;">
|
||||
<label style="display:flex; gap:10px; align-items:flex-start; cursor:pointer;">
|
||||
<input type="checkbox" :checked="selectedRoutes.includes(route.name)" @change="toggleRoute(route.name)" style="margin-top:4px;" />
|
||||
<input type="checkbox" :checked="selectedRoutes.includes(route.name)" :disabled="!selectedRoutes.includes(route.name) && selectedSegmentCount >= maxSegments" @change="toggleRoute(route.name)" style="margin-top:4px;" />
|
||||
<span style="min-width:0;">
|
||||
<strong>{{ fmtDate(route.timestamp) }}</strong>
|
||||
<div class="gx-row__desc" style="word-break:break-all;">{{ route.name }}</div>
|
||||
<div class="gx-row__desc">{{ fmtLen(route) }}</div>
|
||||
<div class="gx-row__desc">{{ fmtLen(route) }} · {{ routeSelectedSegmentCount(route.name) }} selected</div>
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="selectedRoutes.includes(route.name)" style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:8px 0 0 28px;">
|
||||
<span class="gx-row__desc">Segments</span>
|
||||
<input class="gx-field" style="width:92px;" type="number" :min="routeSegmentBounds(route.name).first" :max="routeSegmentBounds(route.name).last" inputmode="numeric" placeholder="First"
|
||||
:value="segmentRanges[route.name]?.start || ''" @input="setSegmentRange(route.name, 'start', $event.target.value)" />
|
||||
<span class="gx-row__desc">to</span>
|
||||
<input class="gx-field" style="width:92px;" type="number" :min="routeSegmentBounds(route.name).first" :max="routeSegmentBounds(route.name).last" inputmode="numeric" placeholder="Last"
|
||||
:value="segmentRanges[route.name]?.end || ''" @input="setSegmentRange(route.name, 'end', $event.target.value)" />
|
||||
<small class="gx-row__desc">Blank uses the whole route.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,7 @@ FLM_STATUS_PATH = Path("/tmp/galaxy_flm_status.json")
|
||||
FLM_LOG_PATH = Path("/tmp/galaxy_flm.log")
|
||||
FLM_STATUS_MAX_AGE_SECONDS = 3600.0
|
||||
FLM_ANALYZER_ROUTE_LIMIT = 8
|
||||
FLM_MAX_ANALYSIS_SEGMENTS = 5
|
||||
FLM_ANALYZER_PROCESS = None
|
||||
FLM_ANALYZER_LOCK = threading.Lock()
|
||||
FLM_PROGRESS_FILENAME = "progress.json"
|
||||
@@ -553,6 +554,42 @@ def normalize_segment_ranges(route_names: list[str], segment_ranges: Any) -> dic
|
||||
return normalized
|
||||
|
||||
|
||||
def selected_segment_count(route_names: list[str], footage_paths: list[str],
|
||||
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> int:
|
||||
normalized = normalize_segment_ranges(route_names, segment_ranges)
|
||||
total = 0
|
||||
for route in route_names[:FLM_ANALYZER_ROUTE_LIMIT]:
|
||||
route = str(route).strip()
|
||||
if not route:
|
||||
continue
|
||||
segments = []
|
||||
for footage_path in footage_paths:
|
||||
candidate = utilities.get_segments_in_route(route, footage_path)
|
||||
if candidate:
|
||||
segments = candidate
|
||||
break
|
||||
|
||||
segment_range = normalized.get(route, {})
|
||||
start = segment_range.get("start")
|
||||
end = segment_range.get("end")
|
||||
if segments:
|
||||
numbers = [_parse_segment_num(segment) for segment in segments]
|
||||
lower = start if start is not None else min(numbers)
|
||||
upper = end if end is not None else max(numbers)
|
||||
total += sum(lower <= number <= upper for number in numbers)
|
||||
elif start is not None and end is not None:
|
||||
total += max(0, end - start + 1)
|
||||
return total
|
||||
|
||||
|
||||
def enforce_segment_limit(route_names: list[str], footage_paths: list[str],
|
||||
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> int:
|
||||
count = selected_segment_count(route_names, footage_paths, segment_ranges)
|
||||
if count > FLM_MAX_ANALYSIS_SEGMENTS:
|
||||
raise ValueError(f"FLM analysis is limited to {FLM_MAX_ANALYSIS_SEGMENTS} segments at a time (requested {count}).")
|
||||
return count
|
||||
|
||||
|
||||
def start_flm_background_analysis(route_names: list[str], footage_paths: list[str],
|
||||
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> bool:
|
||||
global FLM_ANALYZER_PROCESS
|
||||
@@ -567,6 +604,8 @@ def start_flm_background_analysis(route_names: list[str], footage_paths: list[st
|
||||
except FLMAnalysisCancelled:
|
||||
return False
|
||||
|
||||
enforce_segment_limit(route_names, footage_paths, segment_ranges)
|
||||
|
||||
ensure_flm_workspace()
|
||||
process_to_watch = None
|
||||
with FLM_ANALYZER_LOCK:
|
||||
@@ -2424,6 +2463,7 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
segment_ranges = normalize_segment_ranges(route_names, segment_ranges)
|
||||
|
||||
sources, warnings = resolve_route_sources(route_names, footage_paths, segment_ranges)
|
||||
enforce_segment_limit(route_names, footage_paths, segment_ranges)
|
||||
if not sources:
|
||||
raise RuntimeError("No local routes with qlogs or rlogs were found for the selected routes.")
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ def test_process_route_is_metadata_only_and_retains_fields(monkeypatch, tmp_path
|
||||
"isCustomName": True,
|
||||
"is_preserved": True,
|
||||
"segmentCount": 4,
|
||||
"firstSegmentNum": 3,
|
||||
"approxDurationSeconds": 240,
|
||||
}
|
||||
|
||||
|
||||
@@ -232,13 +232,23 @@ def test_segment_ranges_limit_resolved_route_sources(tmp_path, monkeypatch):
|
||||
sources, warnings = module.resolve_route_sources(
|
||||
[route],
|
||||
[str(tmp_path)],
|
||||
{route: {"start": 4, "end": 9}},
|
||||
{route: {"start": 4, "end": 8}},
|
||||
)
|
||||
|
||||
assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8, 9]
|
||||
assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8]
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_segment_limit_rejects_more_than_five_selected_segments(tmp_path, monkeypatch):
|
||||
module, _ = _load_flm_workspace_module(tmp_path)
|
||||
route = "00000001--abcdef1234"
|
||||
segment_names = [f"{route}--{segment}" for segment in range(12)]
|
||||
monkeypatch.setattr(module.utilities, "get_segments_in_route", lambda *_args: segment_names)
|
||||
|
||||
with pytest.raises(ValueError, match="limited to 5 segments"):
|
||||
module.enforce_segment_limit([route], [str(tmp_path)], {route: {"start": 4, "end": 9}})
|
||||
|
||||
|
||||
def test_segment_range_rejects_reversed_bounds(tmp_path):
|
||||
module, _ = _load_flm_workspace_module(tmp_path)
|
||||
with pytest.raises(ValueError, match="first segment"):
|
||||
|
||||
@@ -494,6 +494,12 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
|
||||
"js/components/LateralTuningPanel.js"]:
|
||||
assert "fetch(" not in _read(rel), f"{rel} should not use raw fetch()"
|
||||
|
||||
lateral = _read("js/components/LateralTuningPanel.js")
|
||||
assert "MAX_SEGMENTS = 5" in lateral
|
||||
assert "segmentRanges" in lateral and "selectedSegmentRanges" in lateral
|
||||
assert "flmAnalyze(this.selectedRoutes, this.selectedSegmentRanges())" in lateral
|
||||
assert "routeSelectedSegmentCount" in lateral
|
||||
|
||||
|
||||
def test_ui_cameras_hub_vasm_and_pip_native_no_embed():
|
||||
app = _read("js/app.js")
|
||||
|
||||
@@ -8296,7 +8296,10 @@ def setup(app):
|
||||
except (TypeError, ValueError) as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
|
||||
started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS, segment_ranges)
|
||||
try:
|
||||
started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS, segment_ranges)
|
||||
except (TypeError, ValueError) as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
if not started:
|
||||
return jsonify({"error": "Failed to start FLM analysis."}), 500
|
||||
|
||||
|
||||
@@ -3260,6 +3260,7 @@ def process_route(footage_path, route_name, segment_count=0, first_segment_num=0
|
||||
"isCustomName": custom_name is not None,
|
||||
"is_preserved": has_preserve_attr(segment_path),
|
||||
"segmentCount": max(0, int(segment_count)),
|
||||
"firstSegmentNum": max(0, int(first_segment_num)),
|
||||
"approxDurationSeconds": max(0, int(segment_count)) * 60,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user