@jjlmoya/utils-forensic-science 1.7.0 → 1.9.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 -2
- package/src/category/index.ts +21 -16
- package/src/entries.ts +8 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/time-of-death-algor-mortis-calculator/bibliography.astro +14 -0
- package/src/tool/time-of-death-algor-mortis-calculator/bibliography.ts +7 -0
- package/src/tool/time-of-death-algor-mortis-calculator/component.astro +133 -0
- package/src/tool/time-of-death-algor-mortis-calculator/controller.ts +254 -0
- package/src/tool/time-of-death-algor-mortis-calculator/dom-views.ts +144 -0
- package/src/tool/time-of-death-algor-mortis-calculator/entry.ts +29 -0
- package/src/tool/time-of-death-algor-mortis-calculator/evaluator.ts +34 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/de.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/en.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/es.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/fr.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/id.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/it.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/ja.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/ko.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/nl.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/pl.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/pt.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/ru.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/sv.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/tr.ts +139 -0
- package/src/tool/time-of-death-algor-mortis-calculator/i18n/zh.ts +136 -0
- package/src/tool/time-of-death-algor-mortis-calculator/index.ts +11 -0
- package/src/tool/time-of-death-algor-mortis-calculator/logic.test.ts +77 -0
- package/src/tool/time-of-death-algor-mortis-calculator/logic.ts +177 -0
- package/src/tool/time-of-death-algor-mortis-calculator/seo.astro +15 -0
- package/src/tool/time-of-death-algor-mortis-calculator/storage.ts +21 -0
- package/src/tool/time-of-death-algor-mortis-calculator/time-of-death-algor-mortis-calculator.css +596 -0
- package/src/tool/time-of-death-algor-mortis-calculator/ui.ts +60 -0
- package/src/tools.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-forensic-science",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"check": "astro check",
|
|
29
29
|
"type-check": "astro check",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"preversion": "npm run lint && npm run test",
|
|
31
|
+
"preversion": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"postversion": "git push && git push --tags",
|
|
33
33
|
"patch": "npm version patch",
|
|
34
34
|
"minor": "npm version minor",
|
package/src/category/index.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import type { ScienceCategoryEntry } from '../types';
|
|
2
|
-
import { forensicAgeEstimator } from '../tool/forensic-age-estimator/
|
|
3
|
-
import { widmarkAlcoholSimulator } from '../tool/widmark-alcohol-simulator/
|
|
4
|
-
import { forensicSexDeterminator } from '../tool/forensic-sex-determinator/
|
|
5
|
-
import { forensicStatureEstimator } from '../tool/forensic-stature-estimator/
|
|
6
|
-
import { forensicBloodTestSimulator } from '../tool/forensic-blood-test-simulator/
|
|
7
|
-
import { forensicImageAuthenticityAnalyzer } from '../tool/forensic-image-authenticity-analyzer/
|
|
8
|
-
import { gsrDispersionCalculator } from '../tool/gsr-dispersion-calculator/
|
|
9
|
-
import { forensicTlcInkSimulator } from '../tool/forensic-tlc-ink-simulator/
|
|
10
|
-
import { forensicMicrocrystalDrugSimulator } from '../tool/forensic-microcrystal-drug-simulator/
|
|
11
|
-
import { forensicGlassBeckeLineSimulator } from '../tool/forensic-glass-becke-line-simulator/
|
|
12
|
-
import { forensicFiberComparisonMicroscope } from '../tool/forensic-fiber-comparison-microscope/
|
|
13
|
-
import { bloodstainPatternOriginAnalyzer } from '../tool/bloodstain-pattern-origin-analyzer/
|
|
14
|
-
import { forensicFingerprintMinutiaeIdentifier } from '../tool/forensic-fingerprint-minutiae-identifier/
|
|
15
|
-
import { firePatternOriginAnalyzer } from '../tool/fire-pattern-origin-analyzer/
|
|
16
|
-
import { forensicToolmarkStriationMatcher } from '../tool/forensic-toolmark-striation-matcher/
|
|
2
|
+
import { forensicAgeEstimator } from '../tool/forensic-age-estimator/entry';
|
|
3
|
+
import { widmarkAlcoholSimulator } from '../tool/widmark-alcohol-simulator/entry';
|
|
4
|
+
import { forensicSexDeterminator } from '../tool/forensic-sex-determinator/entry';
|
|
5
|
+
import { forensicStatureEstimator } from '../tool/forensic-stature-estimator/entry';
|
|
6
|
+
import { forensicBloodTestSimulator } from '../tool/forensic-blood-test-simulator/entry';
|
|
7
|
+
import { forensicImageAuthenticityAnalyzer } from '../tool/forensic-image-authenticity-analyzer/entry';
|
|
8
|
+
import { gsrDispersionCalculator } from '../tool/gsr-dispersion-calculator/entry';
|
|
9
|
+
import { forensicTlcInkSimulator } from '../tool/forensic-tlc-ink-simulator/entry';
|
|
10
|
+
import { forensicMicrocrystalDrugSimulator } from '../tool/forensic-microcrystal-drug-simulator/entry';
|
|
11
|
+
import { forensicGlassBeckeLineSimulator } from '../tool/forensic-glass-becke-line-simulator/entry';
|
|
12
|
+
import { forensicFiberComparisonMicroscope } from '../tool/forensic-fiber-comparison-microscope/entry';
|
|
13
|
+
import { bloodstainPatternOriginAnalyzer } from '../tool/bloodstain-pattern-origin-analyzer/entry';
|
|
14
|
+
import { forensicFingerprintMinutiaeIdentifier } from '../tool/forensic-fingerprint-minutiae-identifier/entry';
|
|
15
|
+
import { firePatternOriginAnalyzer } from '../tool/fire-pattern-origin-analyzer/entry';
|
|
16
|
+
import { forensicToolmarkStriationMatcher } from '../tool/forensic-toolmark-striation-matcher/entry';
|
|
17
|
+
import { timeOfDeathAlgorMortisCalculator } from '../tool/time-of-death-algor-mortis-calculator/entry';
|
|
17
18
|
|
|
18
19
|
export const forensicCategory: ScienceCategoryEntry = {
|
|
19
20
|
icon: 'mdi:fingerprint',
|
|
@@ -32,8 +33,12 @@ export const forensicCategory: ScienceCategoryEntry = {
|
|
|
32
33
|
bloodstainPatternOriginAnalyzer,
|
|
33
34
|
forensicFingerprintMinutiaeIdentifier,
|
|
34
35
|
firePatternOriginAnalyzer,
|
|
35
|
-
forensicToolmarkStriationMatcher
|
|
36
|
+
forensicToolmarkStriationMatcher,
|
|
37
|
+
timeOfDeathAlgorMortisCalculator
|
|
36
38
|
],
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
i18n: {
|
|
38
43
|
de: () => import('./i18n/de').then((m) => m.content),
|
|
39
44
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { firePatternOriginAnalyzer } from './tool/fire-pattern-origin-analyzer/e
|
|
|
28
28
|
export type { FirePatternOriginAnalyzerUI, FirePatternOriginAnalyzerLocaleContent } from './tool/fire-pattern-origin-analyzer/entry';
|
|
29
29
|
export { forensicToolmarkStriationMatcher } from './tool/forensic-toolmark-striation-matcher/entry';
|
|
30
30
|
export type { ToolmarkStriationMatcherUI, ToolmarkStriationMatcherLocaleContent } from './tool/forensic-toolmark-striation-matcher/entry';
|
|
31
|
+
export { timeOfDeathAlgorMortisCalculator } from './tool/time-of-death-algor-mortis-calculator/entry';
|
|
32
|
+
export type { TimeOfDeathAlgorMortisUI, TimeOfDeathAlgorMortisLocaleContent } from './tool/time-of-death-algor-mortis-calculator/entry';
|
|
31
33
|
|
|
32
34
|
import { forensicAgeEstimator } from './tool/forensic-age-estimator/entry';
|
|
33
35
|
import { widmarkAlcoholSimulator } from './tool/widmark-alcohol-simulator/entry';
|
|
@@ -44,6 +46,7 @@ import { bloodstainPatternOriginAnalyzer } from './tool/bloodstain-pattern-origi
|
|
|
44
46
|
import { forensicFingerprintMinutiaeIdentifier } from './tool/forensic-fingerprint-minutiae-identifier/entry';
|
|
45
47
|
import { firePatternOriginAnalyzer } from './tool/fire-pattern-origin-analyzer/entry';
|
|
46
48
|
import { forensicToolmarkStriationMatcher } from './tool/forensic-toolmark-striation-matcher/entry';
|
|
49
|
+
import { timeOfDeathAlgorMortisCalculator } from './tool/time-of-death-algor-mortis-calculator/entry';
|
|
47
50
|
|
|
48
51
|
export const ALL_ENTRIES = [
|
|
49
52
|
forensicAgeEstimator,
|
|
@@ -60,5 +63,9 @@ export const ALL_ENTRIES = [
|
|
|
60
63
|
bloodstainPatternOriginAnalyzer,
|
|
61
64
|
forensicFingerprintMinutiaeIdentifier,
|
|
62
65
|
firePatternOriginAnalyzer,
|
|
63
|
-
forensicToolmarkStriationMatcher
|
|
66
|
+
forensicToolmarkStriationMatcher,
|
|
67
|
+
timeOfDeathAlgorMortisCalculator
|
|
64
68
|
];
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 16 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(16);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('scienceCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { timeOfDeathAlgorMortisCalculator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await timeOfDeathAlgorMortisCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'Henssge, C. - Death time estimation in case work by the means of a nomogram', url: 'https://pubmed.ncbi.nlm.nih.gov/3192144/' },
|
|
5
|
+
{ name: 'Henssge, C. & Madea, B. - Estimation of the time since death in the early post-mortem period', url: 'https://pubmed.ncbi.nlm.nih.gov/15364387/' },
|
|
6
|
+
{ name: 'National Center for Biotechnology Information - Postmortem Interval and Algor Mortis Review', url: 'https://www.ncbi.nlm.nih.gov/books/NBK554464/' }
|
|
7
|
+
];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { TimeOfDeathAlgorMortisUI } from './ui';
|
|
4
|
+
import { timeOfDeathAlgorMortisCalculator } from './entry';
|
|
5
|
+
import './time-of-death-algor-mortis-calculator.css';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
ui?: TimeOfDeathAlgorMortisUI;
|
|
9
|
+
locale?: KnownLocale;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { ui: propUi, locale = 'en' } = Astro.props;
|
|
13
|
+
let ui = propUi;
|
|
14
|
+
if (!ui) {
|
|
15
|
+
const content = await timeOfDeathAlgorMortisCalculator.i18n[locale]?.();
|
|
16
|
+
ui = content?.ui;
|
|
17
|
+
}
|
|
18
|
+
if (!ui) return null;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div class="sc-algor-cockpit" id="algor-cockpit-root" data-i18n={JSON.stringify(ui)}>
|
|
22
|
+
<div class="sc-cockpit-sidebar">
|
|
23
|
+
<div>
|
|
24
|
+
<div class="sc-header-row">
|
|
25
|
+
<div class="sc-panel-title">{ui.presetsHeader}</div>
|
|
26
|
+
<div class="sc-unit-toggle">
|
|
27
|
+
<button type="button" class="sc-unit-btn active" data-unit="metric">{ui.unitMetricLabel || 'Metric'}</button>
|
|
28
|
+
<button type="button" class="sc-unit-btn" data-unit="imperial">{ui.unitImperialLabel || 'Imperial'}</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="sc-presets-wrap">
|
|
32
|
+
<button type="button" class="sc-preset-chip active" data-preset="dressedIndoor">{ui.presetDressedIndoor}</button>
|
|
33
|
+
<button type="button" class="sc-preset-chip" data-preset="nakedCalm">{ui.presetNakedCalm}</button>
|
|
34
|
+
<button type="button" class="sc-preset-chip" data-preset="winterOutdoor">{ui.presetWinterOutdoor}</button>
|
|
35
|
+
<button type="button" class="sc-preset-chip" data-preset="submergedWater">{ui.presetSubmergedWater}</button>
|
|
36
|
+
<button type="button" class="sc-preset-chip" data-preset="heavyDuvet">{ui.presetHeavyDuvet}</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="sc-input-group">
|
|
41
|
+
<div class="sc-input-label-row">
|
|
42
|
+
<label class="sc-label" for="slider-rectalTemp">{ui.rectalTempLabel}</label>
|
|
43
|
+
<span class="sc-label" id="label-unit-temp-1">{ui.celsiusUnit}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="sc-tactile-row">
|
|
46
|
+
<input type="range" id="slider-rectalTemp" class="sc-slider" data-key="rectalTemp" min="10" max="40" step="0.1" value="30.5" />
|
|
47
|
+
<input type="number" id="num-rectalTemp" class="sc-num-input" data-key="rectalTemp" aria-label={ui.rectalTempLabel} min="10" max="40" step="0.1" value="30.5" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="sc-input-group">
|
|
52
|
+
<div class="sc-input-label-row">
|
|
53
|
+
<label class="sc-label" for="slider-ambientTemp">{ui.ambientTempLabel}</label>
|
|
54
|
+
<span class="sc-label" id="label-unit-temp-2">{ui.celsiusUnit}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="sc-tactile-row">
|
|
57
|
+
<input type="range" id="slider-ambientTemp" class="sc-slider" data-key="ambientTemp" min="-10" max="38" step="0.1" value="19.5" />
|
|
58
|
+
<input type="number" id="num-ambientTemp" class="sc-num-input" data-key="ambientTemp" aria-label={ui.ambientTempLabel} min="-10" max="38" step="0.1" value="19.5" />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="sc-input-group">
|
|
63
|
+
<div class="sc-input-label-row">
|
|
64
|
+
<label class="sc-label" for="slider-bodyWeight">{ui.bodyWeightLabel}</label>
|
|
65
|
+
<span class="sc-label" id="label-unit-weight">{ui.kgUnit}</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="sc-tactile-row">
|
|
68
|
+
<input type="range" id="slider-bodyWeight" class="sc-slider" data-key="bodyWeight" min="20" max="180" step="0.5" value="75" />
|
|
69
|
+
<input type="number" id="num-bodyWeight" class="sc-num-input" data-key="bodyWeight" aria-label={ui.bodyWeightLabel} min="20" max="180" step="0.5" value="75" />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="sc-input-group">
|
|
74
|
+
<label class="sc-label" for="factor-select-trigger">{ui.factorLabel}</label>
|
|
75
|
+
<div class="sc-custom-select" id="factor-custom-select">
|
|
76
|
+
<button type="button" class="sc-select-trigger" id="factor-select-trigger" aria-haspopup="listbox">
|
|
77
|
+
<span class="sc-select-val">{ui.factorLightClothes} (1.1)</span>
|
|
78
|
+
<svg class="sc-select-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
79
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
<div class="sc-select-dropdown" id="factor-select-dropdown" role="listbox">
|
|
83
|
+
<div class="sc-select-option" data-factor-val="1.0">{ui.factorNaked} (1.0)</div>
|
|
84
|
+
<div class="sc-select-option" data-factor-val="1.1">{ui.factorLightClothes} (1.1)</div>
|
|
85
|
+
<div class="sc-select-option" data-factor-val="1.2">{ui.factorStandardClothes} (1.2)</div>
|
|
86
|
+
<div class="sc-select-option" data-factor-val="1.4">{ui.factorHeavyWinter} (1.4)</div>
|
|
87
|
+
<div class="sc-select-option" data-factor-val="1.3">{ui.factorLightBlanket} (1.3)</div>
|
|
88
|
+
<div class="sc-select-option" data-factor-val="1.8">{ui.factorHeavyDuvet} (1.8)</div>
|
|
89
|
+
<div class="sc-select-option" data-factor-val="0.5">{ui.factorStillWater} (0.5)</div>
|
|
90
|
+
<div class="sc-select-option" data-factor-val="0.35">{ui.factorFlowingWater} (0.35)</div>
|
|
91
|
+
<div class="sc-select-option" data-factor-val="0.7">{ui.factorWetClothing} (0.7)</div>
|
|
92
|
+
<div class="sc-select-option" data-factor-val="0.75">{ui.factorMovingAir} (0.75)</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="sc-input-group">
|
|
98
|
+
<label class="sc-label" for="input-time">{ui.measurementTimeLabel}</label>
|
|
99
|
+
<div class="sc-actions-row">
|
|
100
|
+
<div class="sc-time-wrap">
|
|
101
|
+
<input type="time" id="input-time" class="sc-time-input" value="14:00" />
|
|
102
|
+
<svg class="sc-time-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
103
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
104
|
+
<polyline points="12 6 12 12 16 14"></polyline>
|
|
105
|
+
</svg>
|
|
106
|
+
</div>
|
|
107
|
+
<button type="button" id="btn-now" class="sc-btn sc-btn-primary">{ui.nowBtn}</button>
|
|
108
|
+
<button type="button" id="btn-reset" class="sc-btn">{ui.resetBtn}</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="sc-warning-card">
|
|
113
|
+
<div class="sc-warning-title">{ui.disclaimerTitle || ui.inputsHeader}</div>
|
|
114
|
+
<div class="sc-warning-text">{ui.disclaimer}</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="sc-cockpit-stage">
|
|
119
|
+
<div id="telemetry-stage"></div>
|
|
120
|
+
<div class="sc-chart-stage">
|
|
121
|
+
<div class="sc-panel-title">{ui.chartHeader}</div>
|
|
122
|
+
<svg id="nomogram-svg" class="sc-nomogram-svg"></svg>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
import { AlgorMortisController } from './controller';
|
|
129
|
+
const root = document.getElementById('algor-cockpit-root');
|
|
130
|
+
if (root) {
|
|
131
|
+
new AlgorMortisController(root);
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { TimeOfDeathAlgorMortisUI } from './ui';
|
|
2
|
+
import { calculateAlgorMortis, celsiusToFahrenheit, fahrenheitToCelsius, kgToLb, lbToKg, type AlgorMortisInputs } from './logic';
|
|
3
|
+
import { loadSavedState, saveState } from './storage';
|
|
4
|
+
import { renderTelemetry, renderSvgNomogram } from './dom-views';
|
|
5
|
+
|
|
6
|
+
interface SliderRange {
|
|
7
|
+
min: number;
|
|
8
|
+
max: number;
|
|
9
|
+
step: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PresetDef {
|
|
13
|
+
nameKey: string;
|
|
14
|
+
rectal: number;
|
|
15
|
+
ambient: number;
|
|
16
|
+
weight: number;
|
|
17
|
+
factor: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PRESETS: Record<string, PresetDef> = {
|
|
21
|
+
dressedIndoor: { nameKey: 'presetDressedIndoor', rectal: 30.5, ambient: 19.5, weight: 75, factor: 1.1 },
|
|
22
|
+
nakedCalm: { nameKey: 'presetNakedCalm', rectal: 28.5, ambient: 20.0, weight: 70, factor: 1.0 },
|
|
23
|
+
winterOutdoor: { nameKey: 'presetWinterOutdoor', rectal: 22.0, ambient: 4.0, weight: 80, factor: 1.4 },
|
|
24
|
+
submergedWater: { nameKey: 'presetSubmergedWater', rectal: 24.0, ambient: 12.0, weight: 70, factor: 0.5 },
|
|
25
|
+
heavyDuvet: { nameKey: 'presetHeavyDuvet', rectal: 33.0, ambient: 18.0, weight: 85, factor: 1.8 }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class AlgorMortisController {
|
|
29
|
+
private container: HTMLElement;
|
|
30
|
+
private ui: TimeOfDeathAlgorMortisUI;
|
|
31
|
+
private inputs: AlgorMortisInputs;
|
|
32
|
+
|
|
33
|
+
constructor(container: HTMLElement) {
|
|
34
|
+
this.container = container;
|
|
35
|
+
this.ui = JSON.parse(container.dataset.i18n || '{}');
|
|
36
|
+
this.inputs = this.getInitialInputs();
|
|
37
|
+
this.updateUnitUI();
|
|
38
|
+
this.syncAllFormControls();
|
|
39
|
+
this.bindUnitToggle();
|
|
40
|
+
this.bindInputs();
|
|
41
|
+
this.bindPresets();
|
|
42
|
+
this.bindCustomSelect();
|
|
43
|
+
this.bindActionButtons();
|
|
44
|
+
this.update();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getInitialInputs(): AlgorMortisInputs {
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
50
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
51
|
+
const defaults: AlgorMortisInputs = {
|
|
52
|
+
rectalTemp: 30.5,
|
|
53
|
+
ambientTemp: 19.5,
|
|
54
|
+
bodyWeight: 75,
|
|
55
|
+
correctionFactor: 1.1,
|
|
56
|
+
measurementTime: `${hh}:${mm}`,
|
|
57
|
+
unitSystem: 'metric'
|
|
58
|
+
};
|
|
59
|
+
return Object.assign(defaults, loadSavedState() || {});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private bindUnitToggle(): void {
|
|
63
|
+
this.container.querySelectorAll<HTMLButtonElement>('.sc-unit-btn').forEach((btn) => {
|
|
64
|
+
btn.addEventListener('click', () => {
|
|
65
|
+
const target = btn.dataset.unit as 'metric' | 'imperial';
|
|
66
|
+
if (target !== this.inputs.unitSystem) this.switchUnitSystem(target);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private switchUnitSystem(targetUnit: 'metric' | 'imperial'): void {
|
|
72
|
+
const isTargetImp = targetUnit === 'imperial';
|
|
73
|
+
this.inputs.rectalTemp = isTargetImp ? celsiusToFahrenheit(this.inputs.rectalTemp) : fahrenheitToCelsius(this.inputs.rectalTemp);
|
|
74
|
+
this.inputs.ambientTemp = isTargetImp ? celsiusToFahrenheit(this.inputs.ambientTemp) : fahrenheitToCelsius(this.inputs.ambientTemp);
|
|
75
|
+
this.inputs.bodyWeight = isTargetImp ? kgToLb(this.inputs.bodyWeight) : lbToKg(this.inputs.bodyWeight);
|
|
76
|
+
this.inputs.unitSystem = targetUnit;
|
|
77
|
+
this.updateUnitUI();
|
|
78
|
+
this.syncAllFormControls();
|
|
79
|
+
this.update();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private updateUnitUI(): void {
|
|
83
|
+
const isImp = this.inputs.unitSystem === 'imperial';
|
|
84
|
+
this.container.querySelectorAll<HTMLButtonElement>('.sc-unit-btn').forEach((b) => {
|
|
85
|
+
b.classList.toggle('active', b.dataset.unit === this.inputs.unitSystem);
|
|
86
|
+
});
|
|
87
|
+
this.setText('#label-unit-temp-1', isImp ? this.ui.fahrenheitUnit : this.ui.celsiusUnit);
|
|
88
|
+
this.setText('#label-unit-temp-2', isImp ? this.ui.fahrenheitUnit : this.ui.celsiusUnit);
|
|
89
|
+
this.setText('#label-unit-weight', isImp ? this.ui.lbUnit : this.ui.kgUnit);
|
|
90
|
+
this.setSliderBounds('#slider-rectalTemp', '#num-rectalTemp', isImp ? { min: 50, max: 104, step: 0.2 } : { min: 10, max: 40, step: 0.1 });
|
|
91
|
+
this.setSliderBounds('#slider-ambientTemp', '#num-ambientTemp', isImp ? { min: 14, max: 100, step: 0.2 } : { min: -10, max: 38, step: 0.1 });
|
|
92
|
+
this.setSliderBounds('#slider-bodyWeight', '#num-bodyWeight', isImp ? { min: 44, max: 400, step: 1 } : { min: 20, max: 180, step: 0.5 });
|
|
93
|
+
this.updatePresetLabels();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private updatePresetLabels(): void {
|
|
97
|
+
const isImp = this.inputs.unitSystem === 'imperial';
|
|
98
|
+
this.container.querySelectorAll<HTMLButtonElement>('.sc-preset-chip').forEach((chip) => {
|
|
99
|
+
const p = PRESETS[chip.dataset.preset || ''];
|
|
100
|
+
if (!p) return;
|
|
101
|
+
const rawText = this.ui[p.nameKey];
|
|
102
|
+
const baseName = rawText ? rawText.split('(')[0]?.trim() : p.nameKey;
|
|
103
|
+
const tempDisplay = isImp ? `${celsiusToFahrenheit(p.ambient)}°F` : `${p.ambient}°C`;
|
|
104
|
+
chip.textContent = `${baseName} (${tempDisplay})`;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private setSliderBounds(sId: string, nId: string, range: SliderRange): void {
|
|
109
|
+
const s = this.container.querySelector<HTMLInputElement>(sId);
|
|
110
|
+
const n = this.container.querySelector<HTMLInputElement>(nId);
|
|
111
|
+
[s, n].forEach((el) => {
|
|
112
|
+
if (el) {
|
|
113
|
+
el.min = String(range.min);
|
|
114
|
+
el.max = String(range.max);
|
|
115
|
+
el.step = String(range.step);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private setText(selector: string, val: string): void {
|
|
121
|
+
const el = this.container.querySelector<HTMLElement>(selector);
|
|
122
|
+
if (el) el.textContent = val;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private bindInputs(): void {
|
|
126
|
+
this.container.querySelectorAll<HTMLInputElement>('.sc-slider').forEach((slider) => {
|
|
127
|
+
slider.addEventListener('input', () => this.handleSliderSync(slider));
|
|
128
|
+
});
|
|
129
|
+
this.container.querySelectorAll<HTMLInputElement>('.sc-num-input').forEach((input) => {
|
|
130
|
+
input.addEventListener('input', () => this.handleNumSync(input));
|
|
131
|
+
});
|
|
132
|
+
const timeInput = this.container.querySelector<HTMLInputElement>('#input-time');
|
|
133
|
+
timeInput?.addEventListener('input', () => {
|
|
134
|
+
this.inputs.measurementTime = timeInput.value;
|
|
135
|
+
this.update();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private handleSliderSync(slider: HTMLInputElement): void {
|
|
140
|
+
const key = slider.dataset.key as keyof AlgorMortisInputs;
|
|
141
|
+
if (!key) return;
|
|
142
|
+
const val = parseFloat(slider.value) || 0;
|
|
143
|
+
(this.inputs[key] as number) = val;
|
|
144
|
+
const numInput = this.container.querySelector<HTMLInputElement>(`#num-${key}`);
|
|
145
|
+
if (numInput) numInput.value = String(val);
|
|
146
|
+
this.update();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleNumSync(input: HTMLInputElement): void {
|
|
150
|
+
const key = input.dataset.key as keyof AlgorMortisInputs;
|
|
151
|
+
if (!key) return;
|
|
152
|
+
const val = parseFloat(input.value) || 0;
|
|
153
|
+
(this.inputs[key] as number) = val;
|
|
154
|
+
const slider = this.container.querySelector<HTMLInputElement>(`#slider-${key}`);
|
|
155
|
+
if (slider) slider.value = String(val);
|
|
156
|
+
this.update();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private bindPresets(): void {
|
|
160
|
+
this.container.querySelectorAll<HTMLButtonElement>('.sc-preset-chip').forEach((chip) => {
|
|
161
|
+
chip.addEventListener('click', () => {
|
|
162
|
+
const presetKey = chip.dataset.preset;
|
|
163
|
+
if (!presetKey || !PRESETS[presetKey]) return;
|
|
164
|
+
const p = PRESETS[presetKey];
|
|
165
|
+
const isImp = this.inputs.unitSystem === 'imperial';
|
|
166
|
+
this.inputs.rectalTemp = isImp ? celsiusToFahrenheit(p.rectal) : p.rectal;
|
|
167
|
+
this.inputs.ambientTemp = isImp ? celsiusToFahrenheit(p.ambient) : p.ambient;
|
|
168
|
+
this.inputs.bodyWeight = isImp ? kgToLb(p.weight) : p.weight;
|
|
169
|
+
this.inputs.correctionFactor = p.factor;
|
|
170
|
+
this.syncAllFormControls();
|
|
171
|
+
this.updatePresetChips(presetKey);
|
|
172
|
+
this.update();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private updatePresetChips(activeKey?: string): void {
|
|
178
|
+
this.container.querySelectorAll<HTMLButtonElement>('.sc-preset-chip').forEach((chip) => {
|
|
179
|
+
chip.classList.toggle('active', chip.dataset.preset === activeKey);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private bindCustomSelect(): void {
|
|
184
|
+
const trigger = this.container.querySelector<HTMLButtonElement>('#factor-select-trigger');
|
|
185
|
+
const dropdown = this.container.querySelector<HTMLElement>('#factor-select-dropdown');
|
|
186
|
+
trigger?.addEventListener('click', (e) => {
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
dropdown?.classList.toggle('open');
|
|
189
|
+
});
|
|
190
|
+
document.addEventListener('click', () => dropdown?.classList.remove('open'));
|
|
191
|
+
this.container.querySelectorAll<HTMLElement>(`[data-factor-val]`).forEach((opt) => {
|
|
192
|
+
opt.addEventListener('click', () => {
|
|
193
|
+
this.inputs.correctionFactor = parseFloat(opt.dataset.factorVal || '1.1');
|
|
194
|
+
const span = trigger?.querySelector<HTMLElement>('.sc-select-val');
|
|
195
|
+
if (span) span.textContent = opt.textContent || '';
|
|
196
|
+
dropdown?.classList.remove('open');
|
|
197
|
+
this.update();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private bindActionButtons(): void {
|
|
203
|
+
this.container.querySelector<HTMLButtonElement>('#btn-now')?.addEventListener('click', () => {
|
|
204
|
+
const now = new Date();
|
|
205
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
206
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
207
|
+
this.inputs.measurementTime = `${hh}:${mm}`;
|
|
208
|
+
const timeInput = this.container.querySelector<HTMLInputElement>('#input-time');
|
|
209
|
+
if (timeInput) timeInput.value = this.inputs.measurementTime;
|
|
210
|
+
this.update();
|
|
211
|
+
});
|
|
212
|
+
this.container.querySelector<HTMLButtonElement>('#btn-reset')?.addEventListener('click', () => {
|
|
213
|
+
const isImp = this.inputs.unitSystem === 'imperial';
|
|
214
|
+
this.inputs.rectalTemp = isImp ? 86.9 : 30.5;
|
|
215
|
+
this.inputs.ambientTemp = isImp ? 67.1 : 19.5;
|
|
216
|
+
this.inputs.bodyWeight = isImp ? 165 : 75;
|
|
217
|
+
this.inputs.correctionFactor = 1.1;
|
|
218
|
+
this.inputs.measurementTime = '14:00';
|
|
219
|
+
this.syncAllFormControls();
|
|
220
|
+
this.updatePresetChips();
|
|
221
|
+
this.update();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private syncAllFormControls(): void {
|
|
226
|
+
const syncPair = (key: string, val: number) => {
|
|
227
|
+
const slider = this.container.querySelector<HTMLInputElement>(`#slider-${key}`);
|
|
228
|
+
const num = this.container.querySelector<HTMLInputElement>(`#num-${key}`);
|
|
229
|
+
if (slider) slider.value = String(val);
|
|
230
|
+
if (num) num.value = String(val);
|
|
231
|
+
};
|
|
232
|
+
syncPair('rectalTemp', this.inputs.rectalTemp);
|
|
233
|
+
syncPair('ambientTemp', this.inputs.ambientTemp);
|
|
234
|
+
syncPair('bodyWeight', this.inputs.bodyWeight);
|
|
235
|
+
const timeInput = this.container.querySelector<HTMLInputElement>('#input-time');
|
|
236
|
+
if (timeInput) timeInput.value = this.inputs.measurementTime;
|
|
237
|
+
const trigger = this.container.querySelector<HTMLElement>('#factor-select-trigger .sc-select-val');
|
|
238
|
+
const matchingOpt = this.container.querySelector<HTMLElement>(`[data-factor-val="${this.inputs.correctionFactor}"]`);
|
|
239
|
+
if (trigger && matchingOpt) trigger.textContent = matchingOpt.textContent || '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private update(): void {
|
|
243
|
+
saveState(this.inputs);
|
|
244
|
+
const result = calculateAlgorMortis(this.inputs);
|
|
245
|
+
const telemetryContainer = this.container.querySelector<HTMLElement>('#telemetry-stage');
|
|
246
|
+
if (telemetryContainer) {
|
|
247
|
+
renderTelemetry(telemetryContainer, result, this.inputs, this.ui);
|
|
248
|
+
}
|
|
249
|
+
const svgEl = this.container.querySelector<SVGSVGElement>('#nomogram-svg');
|
|
250
|
+
if (svgEl) {
|
|
251
|
+
renderSvgNomogram(svgEl, { result, inputs: this.inputs, ui: this.ui });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|