-- AD Status View -- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet -- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med -- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, ".."). -- -- snapshot = { -- flows = {{vehicle, action, fillPercent (0-100|nil), level}}, -- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}}, -- chains = {{text, level}}, -- standard rød -- warnings = {{text, level}}, -- standard gul -- } -- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header". ADStatusView = {} ADStatusView.MAX_LINES = 18 ADStatusView.MAX_CHARS = 48 ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen ADStatusView.MIN_FLEX_CHARS = 6 ADStatusView.SEPARATOR = " · " ADStatusView.ELLIPSIS = ".." local LEVELS = {running = true, waiting = true, error = true, stopped = true} local SECTIONS = { {key = "flows", header = "spu_status_flows", defaultLevel = "running"}, {key = "fields", header = "spu_status_fields", defaultLevel = "running"}, {key = "chains", header = "spu_status_chains", defaultLevel = "error"}, {key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"}, } -- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ---- function ADStatusView.textLength(text) local count = 0 for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end return count end local function utf8Prefix(text, chars) local result, count = {}, 0 for char in text:gmatch("[^\128-\191][\128-\191]*") do if count >= chars then break end table.insert(result, char) count = count + 1 end return table.concat(result) end function ADStatusView.fit(text, chars) text = tostring(text or "") if ADStatusView.textLength(text) <= chars then return text end local ellipsis = ADStatusView.ELLIPSIS return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis end -- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares. local function joinFitted(parts, flexIndex) local sep = ADStatusView.SEPARATOR local fixed = ADStatusView.textLength(sep) * (#parts - 1) for index, part in ipairs(parts) do if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end end local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed) local fitted = {} for index, part in ipairs(parts) do fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part end return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS) end local function percentText(value) return string.format("%d %%", math.floor(value + 0.5)) end -- ---- linjer pr. sektion ---- local function flowText(entry) local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")} if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end return joinFitted(parts, 2) end local function fieldText(entry) local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))} if entry.vehicle ~= nil and entry.vehicle ~= "" then table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS)) end if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end return joinFitted(parts, 1) end local function plainText(entry) return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS) end local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText} local function levelOf(entry, default) return LEVELS[entry.level] and entry.level or default end -- Alle linjer uden loft. local function allLines(snapshot, textOf) local lines = {} for _, section in ipairs(SECTIONS) do local entries = snapshot[section.key] or {} if #entries > 0 then table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true}) for _, entry in ipairs(entries) do table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false}) end end end return lines end -- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden -- indhold lige før afkortningen droppes. local function capLines(lines, maxLines, textOf) if #lines <= maxLines then return lines end local kept = {} for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end while #kept > 0 and kept[#kept].header do table.remove(kept) end local hidden = 0 for index = #kept + 1, #lines do if not lines[index].header then hidden = hidden + 1 end end local ok, text = pcall(string.format, textOf("spu_status_more"), hidden) table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false}) return kept end function ADStatusView.lines(snapshot, textOf, maxLines) snapshot = snapshot or {} textOf = textOf or function(key) return key end maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES)) local lines = allLines(snapshot, textOf) if #lines == 0 then return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}} end return capLines(lines, maxLines, textOf) end