@jjlmoya/utils-motor 1.3.0 → 1.5.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 +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +10 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/evChargingTimeCostPlanner/bibliography.astro +6 -0
- package/src/tool/evChargingTimeCostPlanner/bibliography.ts +16 -0
- package/src/tool/evChargingTimeCostPlanner/component.astro +147 -0
- package/src/tool/evChargingTimeCostPlanner/controller.ts +214 -0
- package/src/tool/evChargingTimeCostPlanner/dom-views.ts +124 -0
- package/src/tool/evChargingTimeCostPlanner/entry.ts +30 -0
- package/src/tool/evChargingTimeCostPlanner/ev-charging-time-cost-planner.css +598 -0
- package/src/tool/evChargingTimeCostPlanner/evaluator.ts +10 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/de.ts +22 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/en.ts +161 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/es.ts +16 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/factory.ts +113 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/fr.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/id.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/it.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ja.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ko.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/nl.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/pl.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/pt.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ru.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/sv.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/tr.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/zh.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/index.ts +10 -0
- package/src/tool/evChargingTimeCostPlanner/logic.test.ts +71 -0
- package/src/tool/evChargingTimeCostPlanner/logic.ts +174 -0
- package/src/tool/evChargingTimeCostPlanner/seo.astro +15 -0
- package/src/tool/evChargingTimeCostPlanner/storage.ts +27 -0
- package/src/tool/evChargingTimeCostPlanner/ui.ts +50 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/bibliography.astro +6 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/bibliography.ts +12 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/component.astro +166 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/controller.ts +207 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/dom-views.ts +101 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/entry.ts +30 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/evaluator.ts +7 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/de.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/en.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/es.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/fr.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/id.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/it.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/ja.ts +201 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/ko.ts +201 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/nl.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/pl.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/pt.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/ru.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/sv.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/tr.ts +185 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/i18n/zh.ts +201 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/index.ts +10 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/logic.test.ts +229 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/logic.ts +179 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/motorcycle-skid-mark-speed-estimator.css +523 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/seo.astro +15 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/storage.ts +49 -0
- package/src/tool/motorcycleSkidMarkSpeedEstimator/ui.ts +46 -0
- package/src/tools.ts +5 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CHARGING_PRESETS, calculateChargingPlan } from './logic';
|
|
3
|
+
import { evaluateChargingStatus } from './evaluator';
|
|
4
|
+
|
|
5
|
+
describe('EV charging planner', () => {
|
|
6
|
+
it('calculates battery energy, grid energy, time, cost, losses, and range', () => {
|
|
7
|
+
const plan = calculateChargingPlan(CHARGING_PRESETS[0]!.inputs);
|
|
8
|
+
|
|
9
|
+
expect(plan.chargeFraction).toBeCloseTo(0.48);
|
|
10
|
+
expect(plan.batteryEnergyKwh).toBeCloseTo(28.8);
|
|
11
|
+
expect(plan.gridEnergyKwh).toBeCloseTo(32);
|
|
12
|
+
expect(plan.lossEnergyKwh).toBeCloseTo(3.2);
|
|
13
|
+
expect(plan.timeMinutes).toBe(260);
|
|
14
|
+
expect(plan.cost).toBeCloseTo(8);
|
|
15
|
+
expect(plan.recoveredRangeKm).toBeCloseTo(160);
|
|
16
|
+
expect(evaluateChargingStatus(plan)).toBe('long');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns no charge when the target is below the starting level', () => {
|
|
20
|
+
const plan = calculateChargingPlan({ ...CHARGING_PRESETS[0]!.inputs, startSoc: 80, endSoc: 40 });
|
|
21
|
+
|
|
22
|
+
expect(plan.chargeFraction).toBe(0);
|
|
23
|
+
expect(plan.timeMinutes).toBe(0);
|
|
24
|
+
expect(plan.cost).toBe(0);
|
|
25
|
+
expect(evaluateChargingStatus(plan)).toBe('empty');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('calculates the recovered range from the exact charge window', () => {
|
|
29
|
+
const plan = calculateChargingPlan({ ...CHARGING_PRESETS[1]!.inputs, endSoc: 90 });
|
|
30
|
+
|
|
31
|
+
expect(plan.batteryEnergyKwh).toBeCloseTo(34.8);
|
|
32
|
+
expect(plan.gridEnergyKwh).toBeCloseTo(37.8261, 3);
|
|
33
|
+
expect(plan.recoveredRangeKm).toBeCloseTo(193.3333, 3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('clamps invalid values to safe calculation bounds', () => {
|
|
37
|
+
const plan = calculateChargingPlan({
|
|
38
|
+
capacityKwh: Number.NaN,
|
|
39
|
+
startSoc: -10,
|
|
40
|
+
endSoc: 140,
|
|
41
|
+
chargerPowerKw: 0,
|
|
42
|
+
efficiency: 0,
|
|
43
|
+
pricePerKwh: -1,
|
|
44
|
+
consumptionKwhPer100Km: 0,
|
|
45
|
+
currency: 'USD',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(plan.batteryEnergyKwh).toBe(60);
|
|
49
|
+
expect(plan.gridEnergyKwh).toBe(120);
|
|
50
|
+
expect(plan.timeMinutes).toBe(72000);
|
|
51
|
+
expect(plan.cost).toBe(0);
|
|
52
|
+
expect(plan.recoveredRangeKm).toBe(6000);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('converts the same tariff through the selected currency factor', () => {
|
|
56
|
+
const euroPlan = calculateChargingPlan({ ...CHARGING_PRESETS[0]!.inputs, currency: 'EUR' });
|
|
57
|
+
const dollarPlan = calculateChargingPlan({ ...CHARGING_PRESETS[0]!.inputs, currency: 'USD', pricePerKwh: 0.25 * 1.1643 });
|
|
58
|
+
|
|
59
|
+
expect(dollarPlan.exchangeFactor).toBeCloseTo(1.1643);
|
|
60
|
+
expect(dollarPlan.pricePerKwhEur).toBeCloseTo(0.25);
|
|
61
|
+
expect(dollarPlan.cost).toBeCloseTo(euroPlan.cost * 1.1643);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('classifies short and standard charge windows', () => {
|
|
65
|
+
const shortPlan = calculateChargingPlan({ ...CHARGING_PRESETS[0]!.inputs, startSoc: 50, endSoc: 52, chargerPowerKw: 100 });
|
|
66
|
+
const standardPlan = calculateChargingPlan({ ...CHARGING_PRESETS[0]!.inputs, startSoc: 40, endSoc: 60, chargerPowerKw: 7.4 });
|
|
67
|
+
|
|
68
|
+
expect(evaluateChargingStatus(shortPlan)).toBe('short');
|
|
69
|
+
expect(evaluateChargingStatus(standardPlan)).toBe('standard');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export interface CurrencyOption {
|
|
2
|
+
code: string;
|
|
3
|
+
symbol: string;
|
|
4
|
+
name: string;
|
|
5
|
+
unitsPerEur: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const CURRENCY_OPTIONS = [
|
|
9
|
+
{ code: 'EUR', symbol: '€', name: 'Euro', unitsPerEur: 1 },
|
|
10
|
+
{ code: 'USD', symbol: '$', name: 'US dollar', unitsPerEur: 1.1643 },
|
|
11
|
+
{ code: 'GBP', symbol: '£', name: 'Pound sterling', unitsPerEur: 0.8572 },
|
|
12
|
+
{ code: 'JPY', symbol: '¥', name: 'Japanese yen', unitsPerEur: 185.92 },
|
|
13
|
+
{ code: 'CNY', symbol: 'CN¥', name: 'Chinese yuan', unitsPerEur: 7.8251 },
|
|
14
|
+
{ code: 'INR', symbol: '₹', name: 'Indian rupee', unitsPerEur: 111.0585 },
|
|
15
|
+
{ code: 'BRL', symbol: 'R$', name: 'Brazilian real', unitsPerEur: 6.0126 },
|
|
16
|
+
{ code: 'MXN', symbol: 'MX$', name: 'Mexican peso', unitsPerEur: 19.7327 },
|
|
17
|
+
{ code: 'CAD', symbol: 'CA$', name: 'Canadian dollar', unitsPerEur: 1.613 },
|
|
18
|
+
{ code: 'AUD', symbol: 'A$', name: 'Australian dollar', unitsPerEur: 1.6183 },
|
|
19
|
+
{ code: 'CHF', symbol: 'CHF', name: 'Swiss franc', unitsPerEur: 0.9364 },
|
|
20
|
+
{ code: 'SEK', symbol: 'kr', name: 'Swedish krona', unitsPerEur: 11.0885 },
|
|
21
|
+
{ code: 'PLN', symbol: 'zł', name: 'Polish zloty', unitsPerEur: 4.3365 },
|
|
22
|
+
{ code: 'TRY', symbol: '₺', name: 'Turkish lira', unitsPerEur: 56.1718 },
|
|
23
|
+
{ code: 'KRW', symbol: '₩', name: 'South Korean won', unitsPerEur: 1600.39 },
|
|
24
|
+
{ code: 'IDR', symbol: 'Rp', name: 'Indonesian rupiah', unitsPerEur: 20628.08 },
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type CurrencyCode = typeof CURRENCY_OPTIONS[number]['code'];
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_CURRENCY: CurrencyCode = 'EUR';
|
|
30
|
+
|
|
31
|
+
export function isCurrencyCode(value: unknown): value is CurrencyCode {
|
|
32
|
+
return typeof value === 'string' && CURRENCY_OPTIONS.some((option) => option.code === value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getCurrencyOption(code: CurrencyCode): CurrencyOption {
|
|
36
|
+
return CURRENCY_OPTIONS.find((option) => option.code === code) ?? CURRENCY_OPTIONS[0]!;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function convertCurrencyAmount(amount: number, from: CurrencyCode, to: CurrencyCode): number {
|
|
40
|
+
const fromFactor = getCurrencyOption(from).unitsPerEur;
|
|
41
|
+
const toFactor = getCurrencyOption(to).unitsPerEur;
|
|
42
|
+
return (amount / fromFactor) * toFactor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getPriceInputStep(currency: CurrencyCode): number {
|
|
46
|
+
const factor = getCurrencyOption(currency).unitsPerEur;
|
|
47
|
+
if (factor >= 1000) return 10;
|
|
48
|
+
if (factor >= 100) return 1;
|
|
49
|
+
if (factor >= 10) return 0.1;
|
|
50
|
+
return 0.01;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ChargingInputs {
|
|
54
|
+
capacityKwh: number;
|
|
55
|
+
startSoc: number;
|
|
56
|
+
endSoc: number;
|
|
57
|
+
chargerPowerKw: number;
|
|
58
|
+
efficiency: number;
|
|
59
|
+
pricePerKwh: number;
|
|
60
|
+
consumptionKwhPer100Km: number;
|
|
61
|
+
currency: CurrencyCode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ChargingPlan {
|
|
65
|
+
chargeFraction: number;
|
|
66
|
+
batteryEnergyKwh: number;
|
|
67
|
+
gridEnergyKwh: number;
|
|
68
|
+
lossEnergyKwh: number;
|
|
69
|
+
timeHours: number;
|
|
70
|
+
timeMinutes: number;
|
|
71
|
+
cost: number;
|
|
72
|
+
recoveredRangeKm: number;
|
|
73
|
+
currency: CurrencyCode;
|
|
74
|
+
exchangeFactor: number;
|
|
75
|
+
pricePerKwhEur: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ChargingPreset {
|
|
79
|
+
id: 'home' | 'public' | 'fast';
|
|
80
|
+
inputs: ChargingInputs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const CHARGING_PRESETS: ChargingPreset[] = [
|
|
84
|
+
{
|
|
85
|
+
id: 'home',
|
|
86
|
+
inputs: {
|
|
87
|
+
capacityKwh: 60,
|
|
88
|
+
startSoc: 32,
|
|
89
|
+
endSoc: 80,
|
|
90
|
+
chargerPowerKw: 7.4,
|
|
91
|
+
efficiency: 0.9,
|
|
92
|
+
pricePerKwh: 0.25,
|
|
93
|
+
consumptionKwhPer100Km: 18,
|
|
94
|
+
currency: 'EUR',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'public',
|
|
99
|
+
inputs: {
|
|
100
|
+
capacityKwh: 60,
|
|
101
|
+
startSoc: 32,
|
|
102
|
+
endSoc: 80,
|
|
103
|
+
chargerPowerKw: 11,
|
|
104
|
+
efficiency: 0.92,
|
|
105
|
+
pricePerKwh: 0.39,
|
|
106
|
+
consumptionKwhPer100Km: 18,
|
|
107
|
+
currency: 'EUR',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'fast',
|
|
112
|
+
inputs: {
|
|
113
|
+
capacityKwh: 60,
|
|
114
|
+
startSoc: 32,
|
|
115
|
+
endSoc: 80,
|
|
116
|
+
chargerPowerKw: 100,
|
|
117
|
+
efficiency: 0.94,
|
|
118
|
+
pricePerKwh: 0.55,
|
|
119
|
+
consumptionKwhPer100Km: 18,
|
|
120
|
+
currency: 'EUR',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const finiteOr = (value: number, fallback: number): number => (
|
|
126
|
+
Number.isFinite(value) ? value : fallback
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const clamp = (value: number, minimum: number, maximum: number): number => (
|
|
130
|
+
Math.min(maximum, Math.max(minimum, value))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const safeInputs = (inputs: ChargingInputs): ChargingInputs => ({
|
|
134
|
+
currency: isCurrencyCode(inputs.currency) ? inputs.currency : DEFAULT_CURRENCY,
|
|
135
|
+
capacityKwh: clamp(finiteOr(inputs.capacityKwh, 60), 1, 250),
|
|
136
|
+
startSoc: clamp(finiteOr(inputs.startSoc, 32), 0, 100),
|
|
137
|
+
endSoc: clamp(finiteOr(inputs.endSoc, 80), 0, 100),
|
|
138
|
+
chargerPowerKw: clamp(finiteOr(inputs.chargerPowerKw, 7.4), 0.1, 350),
|
|
139
|
+
efficiency: clamp(finiteOr(inputs.efficiency, 0.9), 0.5, 1),
|
|
140
|
+
pricePerKwh: clamp(
|
|
141
|
+
finiteOr(inputs.pricePerKwh, 0.25 * getCurrencyOption(isCurrencyCode(inputs.currency) ? inputs.currency : DEFAULT_CURRENCY).unitsPerEur),
|
|
142
|
+
0,
|
|
143
|
+
5 * getCurrencyOption(isCurrencyCode(inputs.currency) ? inputs.currency : DEFAULT_CURRENCY).unitsPerEur,
|
|
144
|
+
),
|
|
145
|
+
consumptionKwhPer100Km: clamp(finiteOr(inputs.consumptionKwhPer100Km, 18), 1, 100),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export function calculateChargingPlan(inputs: ChargingInputs): ChargingPlan {
|
|
149
|
+
const values = safeInputs(inputs);
|
|
150
|
+
const chargeFraction = Math.max(0, values.endSoc - values.startSoc) / 100;
|
|
151
|
+
const batteryEnergyKwh = values.capacityKwh * chargeFraction;
|
|
152
|
+
const gridEnergyKwh = batteryEnergyKwh / values.efficiency;
|
|
153
|
+
const lossEnergyKwh = Math.max(0, gridEnergyKwh - batteryEnergyKwh);
|
|
154
|
+
const timeHours = gridEnergyKwh / values.chargerPowerKw;
|
|
155
|
+
const timeMinutes = Math.ceil(timeHours * 60);
|
|
156
|
+
const exchangeFactor = getCurrencyOption(values.currency).unitsPerEur;
|
|
157
|
+
const pricePerKwhEur = values.pricePerKwh / exchangeFactor;
|
|
158
|
+
const cost = gridEnergyKwh * pricePerKwhEur * exchangeFactor;
|
|
159
|
+
const recoveredRangeKm = (batteryEnergyKwh / values.consumptionKwhPer100Km) * 100;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
chargeFraction,
|
|
163
|
+
batteryEnergyKwh,
|
|
164
|
+
gridEnergyKwh,
|
|
165
|
+
lossEnergyKwh,
|
|
166
|
+
timeHours,
|
|
167
|
+
timeMinutes,
|
|
168
|
+
cost,
|
|
169
|
+
recoveredRangeKm,
|
|
170
|
+
currency: values.currency,
|
|
171
|
+
exchangeFactor,
|
|
172
|
+
pricePerKwhEur,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { evChargingTimeCostPlanner } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = evChargingTimeCostPlanner.i18n[locale] ?? evChargingTimeCostPlanner.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ChargingInputs } from './logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya-ev-charging-time-cost-planner';
|
|
4
|
+
|
|
5
|
+
export function loadChargingInputs(): Partial<ChargingInputs> | null {
|
|
6
|
+
try {
|
|
7
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
8
|
+
if (!raw) return null;
|
|
9
|
+
const value: unknown = JSON.parse(raw);
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
11
|
+
return value as Partial<ChargingInputs>;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveChargingInputs(inputs: ChargingInputs): void {
|
|
18
|
+
try {
|
|
19
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clearChargingInputs(): void {
|
|
24
|
+
try {
|
|
25
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface EVChargingTimeCostPlannerUI extends Record<string, string> {
|
|
2
|
+
defaultCurrency: string;
|
|
3
|
+
currencyLabel: string;
|
|
4
|
+
currencyHint: string;
|
|
5
|
+
currencyMenuLabel: string;
|
|
6
|
+
currencyFactorTemplate: string;
|
|
7
|
+
presetMetaTemplate: string;
|
|
8
|
+
presetLabel: string;
|
|
9
|
+
homePreset: string;
|
|
10
|
+
publicPreset: string;
|
|
11
|
+
fastPreset: string;
|
|
12
|
+
resetLabel: string;
|
|
13
|
+
setupLabel: string;
|
|
14
|
+
setupHint: string;
|
|
15
|
+
chargeWindowLabel: string;
|
|
16
|
+
chargeWindowHint: string;
|
|
17
|
+
startSocLabel: string;
|
|
18
|
+
startSocHint: string;
|
|
19
|
+
endSocLabel: string;
|
|
20
|
+
endSocHint: string;
|
|
21
|
+
capacityLabel: string;
|
|
22
|
+
capacityHint: string;
|
|
23
|
+
chargerPowerLabel: string;
|
|
24
|
+
chargerPowerHint: string;
|
|
25
|
+
efficiencyLabel: string;
|
|
26
|
+
efficiencyHint: string;
|
|
27
|
+
priceLabel: string;
|
|
28
|
+
priceHint: string;
|
|
29
|
+
consumptionLabel: string;
|
|
30
|
+
consumptionHint: string;
|
|
31
|
+
resultKicker: string;
|
|
32
|
+
resultTitle: string;
|
|
33
|
+
batterySceneLabel: string;
|
|
34
|
+
startingChargeLabel: string;
|
|
35
|
+
targetChargeLabel: string;
|
|
36
|
+
batteryEnergyLabel: string;
|
|
37
|
+
gridEnergyLabel: string;
|
|
38
|
+
lossesLabel: string;
|
|
39
|
+
timeLabel: string;
|
|
40
|
+
costLabel: string;
|
|
41
|
+
rangeLabel: string;
|
|
42
|
+
statusEmpty: string;
|
|
43
|
+
statusShort: string;
|
|
44
|
+
statusStandard: string;
|
|
45
|
+
statusLong: string;
|
|
46
|
+
summaryTemplate: string;
|
|
47
|
+
formulaLabel: string;
|
|
48
|
+
formulaText: string;
|
|
49
|
+
rangeUnit: string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliographyEntries: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: '49 CFR 571.122 Motorcycle brake systems, including independent front and rear controls, wheel lock, and peak braking coefficient test surfaces',
|
|
6
|
+
url: 'https://www.ecfr.gov/current/title-49/subtitle-B/chapter-V/part-571/subpart-B/section-571.122',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'FHWA Technical Advisory T 5040.38 Pavement Friction Management, locked-wheel method as the analog of emergency braking without ABS',
|
|
10
|
+
url: 'https://www.fhwa.dot.gov/pavement/t504038.cfm',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="trace" data-trace-root data-surface="dry-asphalt" data-unit="metric" data-brake="both" data-eval="ready">
|
|
10
|
+
<div class="trace-card">
|
|
11
|
+
<div class="trace-header">
|
|
12
|
+
<div class="trace-control-group">
|
|
13
|
+
<span class="trace-label-sm">{ui.unitMetric} / {ui.unitImperial}</span>
|
|
14
|
+
<div class="trace-segmented" role="group" aria-label={ui.unitMetric}>
|
|
15
|
+
<button type="button" class="trace-pill" data-unit-choice="metric">
|
|
16
|
+
<span class="trace-pill-title">{ui.unitMetric}</span>
|
|
17
|
+
<span class="trace-pill-unit">{ui.stencilUnitKmh}</span>
|
|
18
|
+
</button>
|
|
19
|
+
<button type="button" class="trace-pill" data-unit-choice="imperial">
|
|
20
|
+
<span class="trace-pill-title">{ui.unitImperial}</span>
|
|
21
|
+
<span class="trace-pill-unit">{ui.stencilUnitMph}</span>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="trace-control-group trace-length-group">
|
|
27
|
+
<label class="trace-length-input-box">
|
|
28
|
+
<span class="trace-input-label">{ui.lengthLabel}</span>
|
|
29
|
+
<div class="trace-input-wrapper">
|
|
30
|
+
<input data-trace-length type="number" min="1" step="0.1" inputmode="decimal" class="trace-numeric-input" />
|
|
31
|
+
<b class="trace-unit-badge" data-length-unit>{ui.lengthUnitM}</b>
|
|
32
|
+
</div>
|
|
33
|
+
</label>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="trace-control-group">
|
|
37
|
+
<span class="trace-label-sm">{ui.brakeLabel}</span>
|
|
38
|
+
<div class="trace-segmented" role="group" aria-label={ui.brakeLabel}>
|
|
39
|
+
<button type="button" class="trace-pill" data-brake-choice="rear">{ui.brakeRear}</button>
|
|
40
|
+
<button type="button" class="trace-pill" data-brake-choice="front">{ui.brakeFront}</button>
|
|
41
|
+
<button type="button" class="trace-pill" data-brake-choice="both">{ui.brakeBoth}</button>
|
|
42
|
+
<button type="button" class="trace-pill" data-brake-choice="unknown">{ui.brakeUnknown}</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="trace-canvas-container" data-trace-road>
|
|
48
|
+
<div class="trace-hud-overlay">
|
|
49
|
+
<div class="trace-hud-badge">
|
|
50
|
+
<span class="trace-hud-caption">{ui.speedLabel}</span>
|
|
51
|
+
<div class="trace-hud-speed" data-trace-speed>56.6 km/h</div>
|
|
52
|
+
<div class="trace-hud-range" data-trace-range>51.2 to 62.0 km/h</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<svg class="trace-svg" viewBox="0 0 1000 340" role="img" aria-label={ui.speedLabel}>
|
|
57
|
+
<defs>
|
|
58
|
+
<pattern id="trace-grit" width="24" height="24" patternUnits="userSpaceOnUse">
|
|
59
|
+
<rect width="24" height="24" class="trace-fill-road"></rect>
|
|
60
|
+
<circle cx="4" cy="6" r="1.5" class="trace-fill-grit" opacity="0.4"></circle>
|
|
61
|
+
<circle cx="16" cy="18" r="1.2" class="trace-fill-grit" opacity="0.3"></circle>
|
|
62
|
+
<circle cx="11" cy="4" r="1" class="trace-fill-grit" opacity="0.5"></circle>
|
|
63
|
+
<circle cx="20" cy="9" r="1.6" class="trace-fill-grit" opacity="0.35"></circle>
|
|
64
|
+
</pattern>
|
|
65
|
+
<linearGradient id="trace-road-shade" x1="0" y1="0" x2="0" y2="1">
|
|
66
|
+
<stop offset="0%" stop-color="#000000" stop-opacity="0.35"></stop>
|
|
67
|
+
<stop offset="50%" stop-color="#000000" stop-opacity="0"></stop>
|
|
68
|
+
<stop offset="100%" stop-color="#000000" stop-opacity="0.4"></stop>
|
|
69
|
+
</linearGradient>
|
|
70
|
+
</defs>
|
|
71
|
+
|
|
72
|
+
<rect class="trace-pavement" x="0" y="0" width="1000" height="340" fill="url(#trace-grit)"></rect>
|
|
73
|
+
<rect class="trace-pavement-shade" x="0" y="0" width="1000" height="340" fill="url(#trace-road-shade)"></rect>
|
|
74
|
+
<path class="trace-lane" d="M20 188 H980"></path>
|
|
75
|
+
|
|
76
|
+
<path class="trace-scar-line trace-scar-rear" pathLength="1" d="M188 248 C 400 242 640 234 940 224"></path>
|
|
77
|
+
<path class="trace-scar-line trace-scar-front" pathLength="1" d="M300 236 C 500 230 720 224 940 216"></path>
|
|
78
|
+
<path class="trace-stria" pathLength="1" d="M190 244 C 400 238 640 230 930 220"></path>
|
|
79
|
+
<path class="trace-stria" pathLength="1" d="M190 252 C 400 246 640 238 930 228"></path>
|
|
80
|
+
|
|
81
|
+
<g class="trace-machine">
|
|
82
|
+
<circle class="trace-tyre" cx="150" cy="210" r="40"></circle>
|
|
83
|
+
<circle class="trace-tyre" cx="278" cy="204" r="38"></circle>
|
|
84
|
+
<circle class="trace-hub" cx="150" cy="210" r="14"></circle>
|
|
85
|
+
<circle class="trace-hub" cx="278" cy="204" r="13"></circle>
|
|
86
|
+
<circle class="trace-rotor trace-rotor-rear" cx="150" cy="210" r="22" data-brake-choice="rear"></circle>
|
|
87
|
+
<circle class="trace-rotor trace-rotor-front" cx="278" cy="204" r="21" data-brake-choice="front"></circle>
|
|
88
|
+
<path class="trace-swing" d="M150 210 L214 194 L278 204"></path>
|
|
89
|
+
<path class="trace-body" d="M168 194 C 186 164 230 152 262 170 L 270 186 L 214 194 L 176 202 Z"></path>
|
|
90
|
+
<path class="trace-saddle" d="M176 180 C 164 172 150 176 144 188 L 168 194 Z"></path>
|
|
91
|
+
<path class="trace-bars" d="M262 170 L 286 156 L 298 158"></path>
|
|
92
|
+
<path class="trace-pipe" d="M188 202 C 210 216 240 218 268 210"></path>
|
|
93
|
+
<circle class="trace-head" cx="248" cy="146" r="11"></circle>
|
|
94
|
+
<path class="trace-rider" d="M248 156 L 232 184 L 200 194 L 226 172 L 258 182"></path>
|
|
95
|
+
</g>
|
|
96
|
+
</svg>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="trace-controls-bottom">
|
|
100
|
+
<div class="trace-surfaces-section">
|
|
101
|
+
<h4 class="trace-section-title">{ui.surfaceLabel}</h4>
|
|
102
|
+
<div class="trace-surface-grid" role="group" aria-label={ui.surfaceLabel}>
|
|
103
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="dry-asphalt" data-tone="dry">
|
|
104
|
+
<span class="trace-surface-dot"></span>
|
|
105
|
+
<span>{ui.surfaceDryAsphalt}</span>
|
|
106
|
+
</button>
|
|
107
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="wet-asphalt" data-tone="wet">
|
|
108
|
+
<span class="trace-surface-dot"></span>
|
|
109
|
+
<span>{ui.surfaceWetAsphalt}</span>
|
|
110
|
+
</button>
|
|
111
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="dry-concrete" data-tone="concrete">
|
|
112
|
+
<span class="trace-surface-dot"></span>
|
|
113
|
+
<span>{ui.surfaceDryConcrete}</span>
|
|
114
|
+
</button>
|
|
115
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="gravel" data-tone="gravel">
|
|
116
|
+
<span class="trace-surface-dot"></span>
|
|
117
|
+
<span>{ui.surfaceGravel}</span>
|
|
118
|
+
</button>
|
|
119
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="grass" data-tone="earth">
|
|
120
|
+
<span class="trace-surface-dot"></span>
|
|
121
|
+
<span>{ui.surfaceGrass}</span>
|
|
122
|
+
</button>
|
|
123
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="ice" data-tone="ice">
|
|
124
|
+
<span class="trace-surface-dot"></span>
|
|
125
|
+
<span>{ui.surfaceIce}</span>
|
|
126
|
+
</button>
|
|
127
|
+
<button type="button" class="trace-surface-chip" data-surface-choice="custom" data-tone="steel">
|
|
128
|
+
<span class="trace-surface-dot"></span>
|
|
129
|
+
<span>{ui.surfaceCustom}</span>
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="trace-sliders-section">
|
|
135
|
+
<div class="trace-slider-card">
|
|
136
|
+
<div class="trace-slider-header">
|
|
137
|
+
<span class="trace-slider-title">{ui.frictionLabel}</span>
|
|
138
|
+
<div class="trace-slider-number-box">
|
|
139
|
+
<input data-trace-friction-number type="number" min="0.05" max="1.2" step="0.01" inputmode="decimal" class="trace-small-input" aria-label={ui.frictionLabel} />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<input data-trace-friction type="range" min="0.05" max="1.2" step="0.01" value="0.70" class="trace-range-slider" aria-label={ui.frictionLabel} />
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="trace-slider-card">
|
|
146
|
+
<div class="trace-slider-header">
|
|
147
|
+
<span class="trace-slider-title">{ui.gradeLabel}</span>
|
|
148
|
+
<span class="trace-slider-val-badge" data-grade-value>0%</span>
|
|
149
|
+
</div>
|
|
150
|
+
<input data-trace-grade type="range" min="-20" max="20" step="1" value="0" class="trace-range-slider" aria-label={ui.gradeLabel} />
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<script is:inline type="application/json" data-trace-ui set:html={JSON.stringify(ui)}></script>
|
|
158
|
+
<script>
|
|
159
|
+
import { mountSkidEstimator } from './controller';
|
|
160
|
+
|
|
161
|
+
const root = document.querySelector('[data-trace-root]');
|
|
162
|
+
const payload = document.querySelector('[data-trace-ui]');
|
|
163
|
+
if (root instanceof HTMLElement && payload?.textContent) {
|
|
164
|
+
mountSkidEstimator(root, JSON.parse(payload.textContent));
|
|
165
|
+
}
|
|
166
|
+
</script>
|