@jjlmoya/utils-forensic-science 1.8.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.
Files changed (35) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +6 -1
  3. package/src/entries.ts +8 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/time-of-death-algor-mortis-calculator/bibliography.astro +14 -0
  7. package/src/tool/time-of-death-algor-mortis-calculator/bibliography.ts +7 -0
  8. package/src/tool/time-of-death-algor-mortis-calculator/component.astro +133 -0
  9. package/src/tool/time-of-death-algor-mortis-calculator/controller.ts +254 -0
  10. package/src/tool/time-of-death-algor-mortis-calculator/dom-views.ts +144 -0
  11. package/src/tool/time-of-death-algor-mortis-calculator/entry.ts +29 -0
  12. package/src/tool/time-of-death-algor-mortis-calculator/evaluator.ts +34 -0
  13. package/src/tool/time-of-death-algor-mortis-calculator/i18n/de.ts +139 -0
  14. package/src/tool/time-of-death-algor-mortis-calculator/i18n/en.ts +136 -0
  15. package/src/tool/time-of-death-algor-mortis-calculator/i18n/es.ts +136 -0
  16. package/src/tool/time-of-death-algor-mortis-calculator/i18n/fr.ts +136 -0
  17. package/src/tool/time-of-death-algor-mortis-calculator/i18n/id.ts +139 -0
  18. package/src/tool/time-of-death-algor-mortis-calculator/i18n/it.ts +136 -0
  19. package/src/tool/time-of-death-algor-mortis-calculator/i18n/ja.ts +136 -0
  20. package/src/tool/time-of-death-algor-mortis-calculator/i18n/ko.ts +136 -0
  21. package/src/tool/time-of-death-algor-mortis-calculator/i18n/nl.ts +139 -0
  22. package/src/tool/time-of-death-algor-mortis-calculator/i18n/pl.ts +139 -0
  23. package/src/tool/time-of-death-algor-mortis-calculator/i18n/pt.ts +136 -0
  24. package/src/tool/time-of-death-algor-mortis-calculator/i18n/ru.ts +139 -0
  25. package/src/tool/time-of-death-algor-mortis-calculator/i18n/sv.ts +139 -0
  26. package/src/tool/time-of-death-algor-mortis-calculator/i18n/tr.ts +139 -0
  27. package/src/tool/time-of-death-algor-mortis-calculator/i18n/zh.ts +136 -0
  28. package/src/tool/time-of-death-algor-mortis-calculator/index.ts +11 -0
  29. package/src/tool/time-of-death-algor-mortis-calculator/logic.test.ts +77 -0
  30. package/src/tool/time-of-death-algor-mortis-calculator/logic.ts +177 -0
  31. package/src/tool/time-of-death-algor-mortis-calculator/seo.astro +15 -0
  32. package/src/tool/time-of-death-algor-mortis-calculator/storage.ts +21 -0
  33. package/src/tool/time-of-death-algor-mortis-calculator/time-of-death-algor-mortis-calculator.css +596 -0
  34. package/src/tool/time-of-death-algor-mortis-calculator/ui.ts +60 -0
  35. package/src/tools.ts +3 -1
@@ -0,0 +1,177 @@
1
+ export interface AlgorMortisInputs {
2
+ rectalTemp: number;
3
+ ambientTemp: number;
4
+ bodyWeight: number;
5
+ correctionFactor: number;
6
+ measurementTime: string;
7
+ unitSystem: 'metric' | 'imperial';
8
+ }
9
+
10
+ export interface CurvePoint {
11
+ time: number;
12
+ temp: number;
13
+ upperTemp: number;
14
+ lowerTemp: number;
15
+ }
16
+
17
+ export interface AlgorMortisResult {
18
+ pmiHours: number;
19
+ confidenceMarginHours: number;
20
+ minPmiHours: number;
21
+ maxPmiHours: number;
22
+ timeOfDeathMin: string;
23
+ timeOfDeathMax: string;
24
+ timeOfDeathEstimated: string;
25
+ coolingPhase: 'plateau' | 'descent' | 'equilibrium' | 'hyperthermia';
26
+ coolingRate: number;
27
+ glaisterPmiHours: number;
28
+ curvePoints: CurvePoint[];
29
+ rectalTempDisplay: number;
30
+ ambientTempDisplay: number;
31
+ refTempDisplay: number;
32
+ coolingRateDisplay: number;
33
+ }
34
+
35
+ export const STANDARD_BODY_TEMP = 37.2;
36
+
37
+ export function celsiusToFahrenheit(c: number): number {
38
+ return Number(((c * 9) / 5 + 32).toFixed(1));
39
+ }
40
+
41
+ export function fahrenheitToCelsius(f: number): number {
42
+ return Number((((f - 32) * 5) / 9).toFixed(1));
43
+ }
44
+
45
+ export function kgToLb(kg: number): number {
46
+ return Number((kg * 2.20462).toFixed(1));
47
+ }
48
+
49
+ export function lbToKg(lb: number): number {
50
+ return Number((lb / 2.20462).toFixed(1));
51
+ }
52
+
53
+ export function computeHenssgeConstant(weightKg: number, factor: number): number {
54
+ const safeWeight = Math.max(5, weightKg);
55
+ const safeFactor = Math.max(0.2, factor);
56
+ const rawK = 1.2815 / (Math.pow(safeWeight, 0.625) * safeFactor) - 0.0284;
57
+ return Math.max(0.005, rawK);
58
+ }
59
+
60
+ export function computeHenssgeQuotient(rectalTemp: number, ambientTemp: number): number {
61
+ const denominator = STANDARD_BODY_TEMP - ambientTemp;
62
+ if (denominator <= 0.1) return 0;
63
+ const quotient = (rectalTemp - ambientTemp) / denominator;
64
+ return Math.min(1.0, Math.max(0.0, quotient));
65
+ }
66
+
67
+ function henssgeEquation(t: number, k: number): number {
68
+ return 1.25 * Math.exp(-k * t) - 0.25 * Math.exp(-5 * k * t);
69
+ }
70
+
71
+ export function solveHenssgeTime(q: number, k: number): number {
72
+ if (q >= 0.999) return 0;
73
+ if (q <= 0.001) return 40;
74
+ let low = 0;
75
+ let high = 60;
76
+ for (let i = 0; i < 40; i++) {
77
+ const mid = (low + high) / 2;
78
+ if (henssgeEquation(mid, k) > q) {
79
+ low = mid;
80
+ } else {
81
+ high = mid;
82
+ }
83
+ }
84
+ return Number(((low + high) / 2).toFixed(2));
85
+ }
86
+
87
+ export function computeConfidenceMargin(pmiHours: number, factor: number): number {
88
+ let baseMargin = 2.8;
89
+ if (pmiHours > 20) {
90
+ baseMargin = 4.5;
91
+ } else if (pmiHours > 10) {
92
+ baseMargin = 3.5;
93
+ }
94
+ const scaled = baseMargin * Math.sqrt(Math.max(0.5, factor));
95
+ return Number(scaled.toFixed(1));
96
+ }
97
+
98
+ export function computeCoolingRate(t: number, k: number, ambientTemp: number): number {
99
+ const delta = STANDARD_BODY_TEMP - ambientTemp;
100
+ const derivative = 1.25 * k * Math.exp(-k * t) - 1.25 * k * Math.exp(-5 * k * t);
101
+ return Number(Math.max(0, delta * derivative).toFixed(2));
102
+ }
103
+
104
+ export function computeGlaisterPmi(rectalTemp: number): number {
105
+ if (rectalTemp >= STANDARD_BODY_TEMP) return 0;
106
+ const diff = STANDARD_BODY_TEMP - rectalTemp;
107
+ return Number(Math.max(0, diff / 0.83).toFixed(2));
108
+ }
109
+
110
+ export function calculateTimeWindow(measurementTime: string, pmiHours: number): string {
111
+ const [hoursStr, minsStr] = measurementTime.split(':');
112
+ const measDate = new Date();
113
+ measDate.setHours(Number(hoursStr) || 12, Number(minsStr) || 0, 0, 0);
114
+ const deathTimeMs = measDate.getTime() - pmiHours * 3600 * 1000;
115
+ const deathDate = new Date(deathTimeMs);
116
+ const hh = String(deathDate.getHours()).padStart(2, '0');
117
+ const mm = String(deathDate.getMinutes()).padStart(2, '0');
118
+ return `${hh}:${mm}`;
119
+ }
120
+
121
+ export function determineCoolingPhase(t: number, rectalTemp: number, ambientTemp: number): 'plateau' | 'descent' | 'equilibrium' | 'hyperthermia' {
122
+ if (rectalTemp > STANDARD_BODY_TEMP) return 'hyperthermia';
123
+ if (rectalTemp <= ambientTemp + 0.5) return 'equilibrium';
124
+ if (t <= 2.5) return 'plateau';
125
+ return 'descent';
126
+ }
127
+
128
+ export function generateCoolingCurve(k: number, ambientTemp: number, maxHours: number, isImperial: boolean): CurvePoint[] {
129
+ const points: CurvePoint[] = [];
130
+ const delta = STANDARD_BODY_TEMP - ambientTemp;
131
+ const step = Math.max(0.5, maxHours / 50);
132
+ for (let t = 0; t <= maxHours; t += step) {
133
+ const q = henssgeEquation(t, k);
134
+ let temp = Number((ambientTemp + delta * q).toFixed(2));
135
+ let upperTemp = Number((ambientTemp + delta * henssgeEquation(Math.max(0, t - 1.5), k)).toFixed(2));
136
+ let lowerTemp = Number((ambientTemp + delta * henssgeEquation(t + 1.5, k)).toFixed(2));
137
+ if (isImperial) {
138
+ temp = celsiusToFahrenheit(temp);
139
+ upperTemp = celsiusToFahrenheit(upperTemp);
140
+ lowerTemp = celsiusToFahrenheit(lowerTemp);
141
+ }
142
+ points.push({ time: Number(t.toFixed(1)), temp, upperTemp, lowerTemp });
143
+ }
144
+ return points;
145
+ }
146
+
147
+ export function calculateAlgorMortis(inputs: AlgorMortisInputs): AlgorMortisResult {
148
+ const isImp = inputs.unitSystem === 'imperial';
149
+ const cRectal = isImp ? fahrenheitToCelsius(inputs.rectalTemp) : inputs.rectalTemp;
150
+ const cAmbient = isImp ? fahrenheitToCelsius(inputs.ambientTemp) : inputs.ambientTemp;
151
+ const kgWeight = isImp ? lbToKg(inputs.bodyWeight) : inputs.bodyWeight;
152
+
153
+ const k = computeHenssgeConstant(kgWeight, inputs.correctionFactor);
154
+ const q = computeHenssgeQuotient(cRectal, cAmbient);
155
+ const pmiHours = solveHenssgeTime(q, k);
156
+ const confidenceMarginHours = computeConfidenceMargin(pmiHours, inputs.correctionFactor);
157
+ const maxCurveTime = Math.max(24, Math.ceil(pmiHours + 8));
158
+ const rateMetric = computeCoolingRate(pmiHours, k, cAmbient);
159
+
160
+ return {
161
+ pmiHours,
162
+ confidenceMarginHours,
163
+ minPmiHours: Math.max(0, Number((pmiHours - confidenceMarginHours).toFixed(2))),
164
+ maxPmiHours: Number((pmiHours + confidenceMarginHours).toFixed(2)),
165
+ timeOfDeathEstimated: calculateTimeWindow(inputs.measurementTime, pmiHours),
166
+ timeOfDeathMin: calculateTimeWindow(inputs.measurementTime, pmiHours + confidenceMarginHours),
167
+ timeOfDeathMax: calculateTimeWindow(inputs.measurementTime, Math.max(0, pmiHours - confidenceMarginHours)),
168
+ coolingPhase: determineCoolingPhase(pmiHours, cRectal, cAmbient),
169
+ coolingRate: rateMetric,
170
+ glaisterPmiHours: computeGlaisterPmi(cRectal),
171
+ curvePoints: generateCoolingCurve(k, cAmbient, maxCurveTime, isImp),
172
+ rectalTempDisplay: inputs.rectalTemp,
173
+ ambientTempDisplay: inputs.ambientTemp,
174
+ refTempDisplay: isImp ? 98.9 : 37.2,
175
+ coolingRateDisplay: isImp ? Number((rateMetric * 1.8).toFixed(2)) : rateMetric
176
+ };
177
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } 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
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,21 @@
1
+ import type { AlgorMortisInputs } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjlmoya_forensic_algor_mortis_state';
4
+
5
+ export function loadSavedState(): Partial<AlgorMortisInputs> | null {
6
+ try {
7
+ const raw = localStorage.getItem(STORAGE_KEY);
8
+ if (!raw) return null;
9
+ return JSON.parse(raw);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export function saveState(inputs: AlgorMortisInputs): void {
16
+ try {
17
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
18
+ } catch {
19
+ return;
20
+ }
21
+ }