@jjlmoya/utils-forensic-science 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/category/i18n/en.ts +3 -3
- package/src/category/i18n/es.ts +2 -2
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/index.ts +61 -59
- package/src/category/seo.astro +1 -1
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/data.ts +1 -1
- package/src/entries.ts +70 -68
- package/src/env.d.ts +1 -1
- package/src/icons/.gitkeep +1 -0
- package/src/index.ts +32 -31
- package/src/pages/[locale]/[slug].astro +161 -159
- package/src/pages/[locale].astro +8 -5
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +76 -0
- package/src/tests/diacritics_density.test.ts +1 -1
- package/src/tests/faq_count.test.ts +19 -19
- package/src/tests/i18n_coverage.test.ts +36 -36
- package/src/tests/inverted_punctuation.test.ts +1 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/script_density.test.ts +94 -94
- package/src/tests/seo_length.test.ts +46 -46
- package/src/tests/seo_parity.test.ts +2 -7
- package/src/tests/seo_translation_completeness.test.ts +68 -0
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tests/translation_copy.test.ts +23 -24
- package/src/tool/bloodstain-pattern-origin-analyzer/component.astro +9 -9
- package/src/tool/bloodstain-pattern-origin-analyzer/i18n/zh.ts +1 -1
- package/src/tool/dna-profile-match-probability-lab/bibliography.astro +14 -0
- package/src/tool/dna-profile-match-probability-lab/bibliography.ts +16 -0
- package/src/tool/dna-profile-match-probability-lab/component.astro +121 -0
- package/src/tool/dna-profile-match-probability-lab/controller.ts +159 -0
- package/src/tool/dna-profile-match-probability-lab/dna-profile-match-probability-lab.css +485 -0
- package/src/tool/dna-profile-match-probability-lab/dom-views.ts +93 -0
- package/src/tool/dna-profile-match-probability-lab/entry.ts +29 -0
- package/src/tool/dna-profile-match-probability-lab/evaluator.ts +19 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/de.ts +43 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/en.ts +196 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/es.ts +40 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/fr.ts +10 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/id.ts +41 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/it.ts +10 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/ja.ts +40 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/ko.ts +40 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/nl.ts +29 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/pl.ts +10 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/pt.ts +41 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/ru.ts +29 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/sv.ts +41 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/tr.ts +10 -0
- package/src/tool/dna-profile-match-probability-lab/i18n/zh.ts +29 -0
- package/src/tool/dna-profile-match-probability-lab/index.ts +11 -0
- package/src/tool/dna-profile-match-probability-lab/localize.ts +107 -0
- package/src/tool/dna-profile-match-probability-lab/logic.test.ts +56 -0
- package/src/tool/dna-profile-match-probability-lab/logic.ts +139 -0
- package/src/tool/dna-profile-match-probability-lab/seo.astro +15 -0
- package/src/tool/dna-profile-match-probability-lab/storage.ts +26 -0
- package/src/tool/dna-profile-match-probability-lab/ui.ts +3 -0
- package/src/tool/fire-pattern-origin-analyzer/component.astro +1 -1
- package/src/tool/fire-pattern-origin-analyzer/i18n/zh.ts +5 -1
- package/src/tool/fire-pattern-origin-analyzer/logic.ts +9 -2
- package/src/tool/fire-pattern-origin-analyzer/view-bindings.ts +8 -4
- package/src/tool/fire-pattern-origin-analyzer/view-model.ts +6 -2
- package/src/tool/forensic-age-estimator/bibliography.astro +1 -1
- package/src/tool/forensic-age-estimator/component.astro +1 -1
- package/src/tool/forensic-age-estimator/entry.ts +17 -17
- package/src/tool/forensic-age-estimator/i18n/de.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/en.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/es.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/fr.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/id.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/it.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/nl.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/pl.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/pt.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/ru.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/sv.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/tr.ts +244 -244
- package/src/tool/forensic-age-estimator/i18n/zh.ts +245 -245
- package/src/tool/forensic-age-estimator/index.ts +1 -1
- package/src/tool/forensic-age-estimator/seo.astro +1 -1
- package/src/tool/forensic-blood-test-simulator/component.astro +1 -1
- package/src/tool/forensic-fiber-comparison-microscope/component.astro +1 -1
- package/src/tool/forensic-fiber-comparison-microscope/render.ts +1 -1
- package/src/tool/forensic-fiber-comparison-microscope/view.ts +1 -1
- package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +2 -2
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +2 -1
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +2 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +9 -8
- package/src/tool/forensic-glass-becke-line-simulator/component.astro +1 -1
- package/src/tool/forensic-glass-becke-line-simulator/view.ts +8 -6
- package/src/tool/forensic-image-authenticity-analyzer/component.astro +1 -1
- package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +95 -90
- package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +7 -2
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +4 -1
- package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +95 -92
- package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +95 -92
- package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -105
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +97 -92
- package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +97 -93
- package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +98 -93
- package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +11 -0
- package/src/tool/forensic-microcrystal-drug-simulator/component.astro +1 -1
- package/src/tool/forensic-sex-determinator/component.astro +3 -2
- package/src/tool/forensic-stature-estimator/component.astro +1 -1
- package/src/tool/forensic-stature-estimator/i18n/id.ts +4 -0
- package/src/tool/forensic-stature-estimator/i18n/nl.ts +4 -0
- package/src/tool/forensic-stature-estimator/i18n/ru.ts +4 -0
- package/src/tool/forensic-tlc-ink-simulator/component.astro +1 -1
- package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +1 -0
- package/src/tool/forensic-toolmark-striation-matcher/component.astro +1 -1
- package/src/tool/forensic-toolmark-striation-matcher/logic.ts +1 -1
- package/src/tool/forensic-toolmark-striation-matcher/renderer.ts +7 -6
- package/src/tool/forensic-toolmark-striation-matcher/view.ts +19 -20
- package/src/tool/gsr-dispersion-calculator/component.astro +1 -1
- package/src/tool/gsr-dispersion-calculator/logic.ts +1 -1
- package/src/tool/time-of-death-algor-mortis-calculator/entry.ts +2 -0
- package/src/tool/widmark-alcohol-simulator/component.astro +1 -1
- package/src/tool/widmark-alcohol-simulator/i18n/de.ts +5 -0
- package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +2 -1
- package/src/tool/widmark-alcohol-simulator/i18n/id.ts +1 -0
- package/src/tool/widmark-alcohol-simulator/i18n/it.ts +4 -1
- package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +4 -1
- package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +5 -1
- package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +5 -1
- package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +5 -1
- package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +4 -1
- package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +5 -1
- package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +4 -1
- package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +4 -1
- package/src/tools.ts +37 -35
- package/src/types.ts +4 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './dna-profile-match-probability-lab.css';
|
|
3
|
+
import { PROFILE_PRESETS, calculateProfile } from './logic';
|
|
4
|
+
import type { DnaProfileMatchProbabilityUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui: DnaProfileMatchProbabilityUI;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
const initialPreset = PROFILE_PRESETS[0]!;
|
|
12
|
+
const initialResult = calculateProfile(initialPreset.loci);
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="dna-match-tool" id="dna-profile-match-lab">
|
|
16
|
+
<section class="dna-match-controls" aria-label={ui.controls}>
|
|
17
|
+
<div class="dna-match-control-head">
|
|
18
|
+
<div>
|
|
19
|
+
<span class="dna-match-kicker">{ui.controlKicker}</span>
|
|
20
|
+
<p class="dna-match-helper">{ui.controlHelper}</p>
|
|
21
|
+
</div>
|
|
22
|
+
<span class="dna-match-locus-count" data-locus-count>{initialPreset.loci.length} {ui.loci}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="dna-match-presets" aria-label={ui.presets}>
|
|
26
|
+
{PROFILE_PRESETS.map((preset, index) => (
|
|
27
|
+
<button type="button" class:list={{ 'is-active': index === 0 }} data-preset={preset.id}>
|
|
28
|
+
<span>{ui[`preset${preset.id}`]}</span>
|
|
29
|
+
<small>{preset.loci.length} {ui.loci}</small>
|
|
30
|
+
</button>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="dna-match-locus-list" data-locus-list>
|
|
35
|
+
{initialPreset.loci.map((locus, index) => (
|
|
36
|
+
<details class="dna-match-locus" data-locus-card open={index === 0}>
|
|
37
|
+
<summary>
|
|
38
|
+
<span data-locus-title>{locus.name}</span>
|
|
39
|
+
<span data-locus-summary>{locus.allele1} / {locus.allele2}</span>
|
|
40
|
+
</summary>
|
|
41
|
+
<div class="dna-match-locus-fields">
|
|
42
|
+
<label class="dna-match-field">
|
|
43
|
+
<span>{ui.locusName}</span>
|
|
44
|
+
<input data-field="name" type="text" value={locus.name} />
|
|
45
|
+
</label>
|
|
46
|
+
<label class="dna-match-field">
|
|
47
|
+
<span>{ui.alleleA}</span>
|
|
48
|
+
<input data-field="allele1" type="text" value={locus.allele1} />
|
|
49
|
+
</label>
|
|
50
|
+
<label class="dna-match-field">
|
|
51
|
+
<span>{ui.alleleB}</span>
|
|
52
|
+
<input data-field="allele2" type="text" value={locus.allele2} />
|
|
53
|
+
</label>
|
|
54
|
+
<label class="dna-match-field">
|
|
55
|
+
<span>{ui.frequencyA}</span>
|
|
56
|
+
<input data-field="frequency1" type="number" min="0.01" max="100" step="0.01" value={locus.frequency1 * 100} />
|
|
57
|
+
</label>
|
|
58
|
+
<label class="dna-match-field">
|
|
59
|
+
<span>{ui.frequencyB}</span>
|
|
60
|
+
<input data-field="frequency2" type="number" min="0.01" max="100" step="0.01" value={locus.frequency2 * 100} />
|
|
61
|
+
</label>
|
|
62
|
+
<button type="button" class="dna-match-remove" data-remove-locus>{ui.removeLocus}</button>
|
|
63
|
+
</div>
|
|
64
|
+
</details>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<button type="button" class="dna-match-add" data-add-locus>{ui.addLocus}</button>
|
|
69
|
+
<p class="dna-match-disclaimer">{ui.disclaimer}</p>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section class="dna-match-scene" aria-label={ui.electropherogram}>
|
|
73
|
+
<div class="dna-match-scene-header">
|
|
74
|
+
<div>
|
|
75
|
+
<span class="dna-match-kicker">{ui.sceneKicker}</span>
|
|
76
|
+
<h2>{ui.electropherogram}</h2>
|
|
77
|
+
</div>
|
|
78
|
+
<span class="dna-match-scene-note">{ui.sceneNote}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="dna-match-electropherogram" data-electropherogram role="img" aria-label={ui.electropherogram}></div>
|
|
81
|
+
<div class="dna-match-legend">
|
|
82
|
+
<span><i class="dna-match-legend-dot dna-match-legend-dot-coral"></i>{ui.alleleA}</span>
|
|
83
|
+
<span><i class="dna-match-legend-dot dna-match-legend-dot-cyan"></i>{ui.alleleB}</span>
|
|
84
|
+
<span>{ui.peakHeight}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
|
|
88
|
+
<section class="dna-match-results" aria-label={ui.results}>
|
|
89
|
+
<div class="dna-match-result-lead">
|
|
90
|
+
<span class="dna-match-kicker">{ui.resultKicker}</span>
|
|
91
|
+
<output class="dna-match-combined" data-result="combined" aria-live="polite">-</output>
|
|
92
|
+
<p data-result="probability">-</p>
|
|
93
|
+
<span class="dna-match-badge" data-result="badge">-</span>
|
|
94
|
+
<p class="dna-match-result-detail" data-result="detail">-</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="dna-match-table-wrap">
|
|
97
|
+
<table>
|
|
98
|
+
<caption>{ui.locusBreakdown}</caption>
|
|
99
|
+
<thead>
|
|
100
|
+
<tr><th scope="col">{ui.locus}</th><th scope="col">{ui.genotype}</th><th scope="col">{ui.rule}</th><th scope="col">{ui.frequency}</th></tr>
|
|
101
|
+
</thead>
|
|
102
|
+
<tbody data-result="table">
|
|
103
|
+
{initialResult.loci.map((locus) => (
|
|
104
|
+
<tr><td>{locus.name}</td><td>{locus.allele1} / {locus.allele2}</td><td>{locus.formula}</td><td>{(locus.genotypeFrequency * 100).toFixed(2)}%</td></tr>
|
|
105
|
+
))}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<script is:inline id="dna-profile-match-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
|
|
113
|
+
|
|
114
|
+
<script>
|
|
115
|
+
import { startDnaProfileMatch } from './controller';
|
|
116
|
+
|
|
117
|
+
const root = document.getElementById('dna-profile-match-lab');
|
|
118
|
+
const uiElement = document.getElementById('dna-profile-match-ui');
|
|
119
|
+
const ui = JSON.parse(uiElement?.textContent ?? '{}') as Record<string, string>;
|
|
120
|
+
if (root) startDnaProfileMatch(root, ui);
|
|
121
|
+
</script>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PRESET_ID,
|
|
3
|
+
MAX_LOCI,
|
|
4
|
+
MIN_LOCI,
|
|
5
|
+
calculateProfile,
|
|
6
|
+
getPreset,
|
|
7
|
+
type LocusInput,
|
|
8
|
+
} from './logic';
|
|
9
|
+
import { renderElectropherogram, renderResults, type ResultTargets } from './dom-views';
|
|
10
|
+
import { loadProfileState, saveProfileState, type SavedProfileState } from './storage';
|
|
11
|
+
import type { DnaProfileMatchProbabilityUI } from './ui';
|
|
12
|
+
|
|
13
|
+
function uiText(ui: DnaProfileMatchProbabilityUI, key: string): string {
|
|
14
|
+
return ui[key] ?? '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeHtml(value: string): string {
|
|
18
|
+
return value.replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] ?? '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function inputValue(card: HTMLElement, field: string): string {
|
|
22
|
+
return card.querySelector<HTMLInputElement>(`[data-field="${field}"]`)?.value.trim() ?? '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readLocus(card: HTMLElement): LocusInput {
|
|
26
|
+
return {
|
|
27
|
+
name: inputValue(card, 'name'),
|
|
28
|
+
allele1: inputValue(card, 'allele1'),
|
|
29
|
+
allele2: inputValue(card, 'allele2'),
|
|
30
|
+
frequency1: Number(inputValue(card, 'frequency1')) / 100,
|
|
31
|
+
frequency2: Number(inputValue(card, 'frequency2')) / 100,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readLoci(root: HTMLElement): LocusInput[] {
|
|
36
|
+
return [...root.querySelectorAll<HTMLElement>('[data-locus-card]')].map(readLocus);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fieldMarkup(label: string, field: string, value: string, type: string): string {
|
|
40
|
+
return `<label class="dna-match-field"><span>${escapeHtml(label)}</span><input data-field="${field}" type="${type}" value="${escapeHtml(value)}" ${type === 'number' ? 'min="0.01" max="100" step="0.01"' : ''} /></label>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function locusMarkup(locus: LocusInput, index: number, ui: DnaProfileMatchProbabilityUI): string {
|
|
44
|
+
const name = locus.name || `${uiText(ui, 'locus')} ${index + 1}`;
|
|
45
|
+
return `<details class="dna-match-locus" data-locus-card ${index === 0 ? 'open' : ''}><summary><span data-locus-title>${escapeHtml(name)}</span><span data-locus-summary>${escapeHtml(locus.allele1)} / ${escapeHtml(locus.allele2)}</span></summary><div class="dna-match-locus-fields">${fieldMarkup(uiText(ui, 'locusName'), 'name', locus.name, 'text')}${fieldMarkup(uiText(ui, 'alleleA'), 'allele1', locus.allele1, 'text')}${fieldMarkup(uiText(ui, 'alleleB'), 'allele2', locus.allele2, 'text')}${fieldMarkup(uiText(ui, 'frequencyA'), 'frequency1', String(locus.frequency1 * 100), 'number')}${fieldMarkup(uiText(ui, 'frequencyB'), 'frequency2', String(locus.frequency2 * 100), 'number')}<button type="button" class="dna-match-remove" data-remove-locus>${escapeHtml(uiText(ui, 'removeLocus'))}</button></div></details>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function appendLocus(list: HTMLElement, locus: LocusInput, index: number, ui: DnaProfileMatchProbabilityUI): void {
|
|
49
|
+
list.insertAdjacentHTML('beforeend', locusMarkup(locus, index, ui));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function replaceLoci(list: HTMLElement, loci: LocusInput[], ui: DnaProfileMatchProbabilityUI): void {
|
|
53
|
+
list.replaceChildren();
|
|
54
|
+
loci.forEach((locus, index) => appendLocus(list, locus, index, ui));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateRemoveButtons(root: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
58
|
+
const disabled = root.querySelectorAll('[data-locus-card]').length <= MIN_LOCI;
|
|
59
|
+
root.querySelectorAll<HTMLButtonElement>('[data-remove-locus]').forEach((button) => {
|
|
60
|
+
button.disabled = disabled;
|
|
61
|
+
});
|
|
62
|
+
const count = root.querySelector<HTMLElement>('[data-locus-count]');
|
|
63
|
+
if (count) count.textContent = `${root.querySelectorAll('[data-locus-card]').length} ${uiText(ui, 'loci')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function updateLocusSummaries(root: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
67
|
+
root.querySelectorAll<HTMLElement>('[data-locus-card]').forEach((card, index) => {
|
|
68
|
+
const name = inputValue(card, 'name') || `${uiText(ui, 'locus')} ${index + 1}`;
|
|
69
|
+
const alleles = `${inputValue(card, 'allele1') || '?'} / ${inputValue(card, 'allele2') || '?'}`;
|
|
70
|
+
const title = card.querySelector<HTMLElement>('[data-locus-title]');
|
|
71
|
+
const summary = card.querySelector<HTMLElement>('[data-locus-summary]');
|
|
72
|
+
if (title) title.textContent = name;
|
|
73
|
+
if (summary) summary.textContent = alleles;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function profilesMatch(first: LocusInput[], second: LocusInput[]): boolean {
|
|
78
|
+
return first.length === second.length && first.every((locus, index) => {
|
|
79
|
+
const other = second[index];
|
|
80
|
+
return other !== undefined && JSON.stringify(locus) === JSON.stringify(other);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function syncPresetButtons(root: HTMLElement, loci: LocusInput[]): void {
|
|
85
|
+
root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => {
|
|
86
|
+
const preset = getPreset(button.dataset.preset ?? DEFAULT_PRESET_ID);
|
|
87
|
+
button.classList.toggle('is-active', profilesMatch(loci, preset.loci));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resultTargets(root: HTMLElement): ResultTargets | null {
|
|
92
|
+
const badge = root.querySelector<HTMLElement>('[data-result="badge"]');
|
|
93
|
+
const detail = root.querySelector<HTMLElement>('[data-result="detail"]');
|
|
94
|
+
const combined = root.querySelector<HTMLElement>('[data-result="combined"]');
|
|
95
|
+
const probability = root.querySelector<HTMLElement>('[data-result="probability"]');
|
|
96
|
+
const table = root.querySelector<HTMLTableSectionElement>('[data-result="table"]');
|
|
97
|
+
if (!badge || !detail || !combined || !probability || !table) return null;
|
|
98
|
+
return { badge, detail, combined, probability, table };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function update(root: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
102
|
+
const loci = readLoci(root);
|
|
103
|
+
const result = calculateProfile(loci);
|
|
104
|
+
const scene = root.querySelector<HTMLElement>('[data-electropherogram]');
|
|
105
|
+
const targets = resultTargets(root);
|
|
106
|
+
if (scene) renderElectropherogram(scene, loci, ui);
|
|
107
|
+
if (targets) renderResults(targets, result, ui);
|
|
108
|
+
updateLocusSummaries(root, ui);
|
|
109
|
+
saveProfileState({ loci });
|
|
110
|
+
updateRemoveButtons(root, ui);
|
|
111
|
+
syncPresetButtons(root, loci);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function bindPresetButtons(root: HTMLElement, list: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
115
|
+
root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => {
|
|
116
|
+
button.addEventListener('click', () => {
|
|
117
|
+
replaceLoci(list, getPreset(button.dataset.preset ?? DEFAULT_PRESET_ID).loci, ui);
|
|
118
|
+
root.querySelectorAll('[data-preset]').forEach((item) => item.classList.toggle('is-active', item === button));
|
|
119
|
+
update(root, ui);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bindLocusActions(root: HTMLElement, list: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
125
|
+
list.addEventListener('input', () => update(root, ui));
|
|
126
|
+
list.addEventListener('click', (event) => {
|
|
127
|
+
const target = event.target as HTMLElement;
|
|
128
|
+
const remove = target.closest<HTMLButtonElement>('[data-remove-locus]');
|
|
129
|
+
if (!remove) return;
|
|
130
|
+
remove.closest('[data-locus-card]')?.remove();
|
|
131
|
+
update(root, ui);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function bindAddButton(root: HTMLElement, list: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
136
|
+
root.querySelector<HTMLButtonElement>('[data-add-locus]')?.addEventListener('click', () => {
|
|
137
|
+
if (list.querySelectorAll('[data-locus-card]').length >= MAX_LOCI) return;
|
|
138
|
+
const index = list.querySelectorAll('[data-locus-card]').length;
|
|
139
|
+
appendLocus(list, { name: `${uiText(ui, 'locus')} ${index + 1}`, allele1: '', allele2: '', frequency1: 0, frequency2: 0 }, index, ui);
|
|
140
|
+
update(root, ui);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function restore(list: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
145
|
+
const fallback: SavedProfileState = { loci: getPreset(DEFAULT_PRESET_ID).loci };
|
|
146
|
+
const saved = loadProfileState(fallback);
|
|
147
|
+
const loci = saved.loci.length >= MIN_LOCI && saved.loci.length <= MAX_LOCI ? saved.loci : fallback.loci;
|
|
148
|
+
replaceLoci(list, loci, ui);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function startDnaProfileMatch(root: HTMLElement, ui: DnaProfileMatchProbabilityUI): void {
|
|
152
|
+
const list = root.querySelector<HTMLElement>('[data-locus-list]');
|
|
153
|
+
if (!list) return;
|
|
154
|
+
restore(list, ui);
|
|
155
|
+
bindPresetButtons(root, list, ui);
|
|
156
|
+
bindLocusActions(root, list, ui);
|
|
157
|
+
bindAddButton(root, list, ui);
|
|
158
|
+
update(root, ui);
|
|
159
|
+
}
|