@jjlmoya/utils-astronomy 1.35.1 → 1.37.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 (86) hide show
  1. package/package.json +18 -5
  2. package/scripts/postbuild.mjs +17 -0
  3. package/scripts/postinstall.mjs +2 -4
  4. package/src/category/index.ts +2 -1
  5. package/src/components/ProductionBreadcrumb.astro +134 -0
  6. package/src/components/ProductionStructuredData.astro +46 -0
  7. package/src/components/ProductionWidget.astro +182 -0
  8. package/src/entries.ts +4 -2
  9. package/src/i18n/header-ui.ts +15 -0
  10. package/src/i18n/language-ui.ts +9 -0
  11. package/src/i18n/languages.ts +24 -0
  12. package/src/identity/brands.ts +5 -0
  13. package/src/index.ts +2 -1
  14. package/src/layouts/ProductionCategoryPage.astro +184 -0
  15. package/src/layouts/ProductionPage.astro +218 -0
  16. package/src/layouts/ProductionUtilityPage.astro +283 -0
  17. package/src/mfe/assets.ts +39 -0
  18. package/src/mfe/category-ui.ts +34 -0
  19. package/src/mfe/manifest.ts +45 -0
  20. package/src/mfe/routes.ts +50 -0
  21. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
  22. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  23. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  24. package/src/pages/index.astro +4 -4
  25. package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
  26. package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
  27. package/src/pages/utilidades/[slug].astro +90 -0
  28. package/src/pages/utilidades/categorias/[category].astro +38 -0
  29. package/src/tests/locale_completeness.test.ts +2 -2
  30. package/src/tests/mfe_cache_contract.test.ts +14 -0
  31. package/src/tests/mfe_manifest.test.ts +28 -0
  32. package/src/tests/registry_contract.test.ts +67 -0
  33. package/src/tests/tool_validation.test.ts +4 -3
  34. package/src/tool/astrophotographyExposureStackPlanner/astrophotography-exposure-stacking-planner.css +496 -0
  35. package/src/tool/astrophotographyExposureStackPlanner/bibliography.astro +14 -0
  36. package/src/tool/astrophotographyExposureStackPlanner/bibliography.ts +12 -0
  37. package/src/tool/astrophotographyExposureStackPlanner/component.astro +149 -0
  38. package/src/tool/astrophotographyExposureStackPlanner/controller.ts +125 -0
  39. package/src/tool/astrophotographyExposureStackPlanner/dom-views.ts +41 -0
  40. package/src/tool/astrophotographyExposureStackPlanner/entry.ts +29 -0
  41. package/src/tool/astrophotographyExposureStackPlanner/evaluator.ts +44 -0
  42. package/src/tool/astrophotographyExposureStackPlanner/i18n/de.ts +40 -0
  43. package/src/tool/astrophotographyExposureStackPlanner/i18n/en.ts +232 -0
  44. package/src/tool/astrophotographyExposureStackPlanner/i18n/es.ts +52 -0
  45. package/src/tool/astrophotographyExposureStackPlanner/i18n/fr.ts +40 -0
  46. package/src/tool/astrophotographyExposureStackPlanner/i18n/id.ts +40 -0
  47. package/src/tool/astrophotographyExposureStackPlanner/i18n/it.ts +40 -0
  48. package/src/tool/astrophotographyExposureStackPlanner/i18n/ja.ts +40 -0
  49. package/src/tool/astrophotographyExposureStackPlanner/i18n/ko.ts +40 -0
  50. package/src/tool/astrophotographyExposureStackPlanner/i18n/nl.ts +40 -0
  51. package/src/tool/astrophotographyExposureStackPlanner/i18n/pl.ts +40 -0
  52. package/src/tool/astrophotographyExposureStackPlanner/i18n/pt.ts +40 -0
  53. package/src/tool/astrophotographyExposureStackPlanner/i18n/ru.ts +40 -0
  54. package/src/tool/astrophotographyExposureStackPlanner/i18n/sv.ts +40 -0
  55. package/src/tool/astrophotographyExposureStackPlanner/i18n/tr.ts +40 -0
  56. package/src/tool/astrophotographyExposureStackPlanner/i18n/zh.ts +40 -0
  57. package/src/tool/astrophotographyExposureStackPlanner/index.ts +11 -0
  58. package/src/tool/astrophotographyExposureStackPlanner/localized.ts +59 -0
  59. package/src/tool/astrophotographyExposureStackPlanner/logic.test.ts +74 -0
  60. package/src/tool/astrophotographyExposureStackPlanner/logic.ts +95 -0
  61. package/src/tool/astrophotographyExposureStackPlanner/seo.astro +14 -0
  62. package/src/tool/astrophotographyExposureStackPlanner/storage.ts +22 -0
  63. package/src/tool/astrophotographyExposureStackPlanner/ui.ts +102 -0
  64. package/src/tool/meteorShowerObservingPlanner/dom-views.ts +38 -16
  65. package/src/tool/meteorShowerObservingPlanner/i18n/de.ts +219 -219
  66. package/src/tool/meteorShowerObservingPlanner/i18n/en.ts +219 -219
  67. package/src/tool/meteorShowerObservingPlanner/i18n/es.ts +219 -219
  68. package/src/tool/meteorShowerObservingPlanner/i18n/fr.ts +219 -219
  69. package/src/tool/meteorShowerObservingPlanner/i18n/id.ts +223 -223
  70. package/src/tool/meteorShowerObservingPlanner/i18n/it.ts +223 -223
  71. package/src/tool/meteorShowerObservingPlanner/i18n/ja.ts +219 -219
  72. package/src/tool/meteorShowerObservingPlanner/i18n/ko.ts +219 -219
  73. package/src/tool/meteorShowerObservingPlanner/i18n/nl.ts +223 -223
  74. package/src/tool/meteorShowerObservingPlanner/i18n/pl.ts +223 -223
  75. package/src/tool/meteorShowerObservingPlanner/i18n/pt.ts +223 -223
  76. package/src/tool/meteorShowerObservingPlanner/i18n/ru.ts +219 -219
  77. package/src/tool/meteorShowerObservingPlanner/i18n/sv.ts +223 -223
  78. package/src/tool/meteorShowerObservingPlanner/i18n/tr.ts +223 -223
  79. package/src/tool/meteorShowerObservingPlanner/i18n/zh.ts +219 -219
  80. package/src/tool/meteorShowerObservingPlanner/meteor-shower-observing-planner.css +16 -9
  81. package/src/tool/meteorShowerObservingPlanner/ui.ts +87 -87
  82. package/src/tools.ts +3 -1
  83. package/src/types.ts +2 -2
  84. package/src/worker.ts +27 -0
  85. package/src/pages/[locale]/[slug].astro +0 -161
  86. package/src/pages/[locale].astro +0 -251
@@ -0,0 +1,95 @@
1
+ import type {
2
+ ExposurePlannerInput,
3
+ ExposurePlannerResult,
4
+ SensorPreset,
5
+ TimelineSlice,
6
+ } from './ui';
7
+
8
+ export const SENSOR_CROP_FACTORS: Record<SensorPreset, number> = {
9
+ fullFrame: 1,
10
+ apsC: 1.5,
11
+ microFourThirds: 2,
12
+ phone: 2.7,
13
+ };
14
+
15
+ export const DEFAULT_INPUT: ExposurePlannerInput = {
16
+ exposureRule: 'npf',
17
+ sensor: 'fullFrame',
18
+ focalLength: 24,
19
+ aperture: 2.8,
20
+ pixelPitch: 5.9,
21
+ declination: 0,
22
+ tracking: 'untracked',
23
+ trackingExposureSeconds: 120,
24
+ deadTimeSeconds: 2,
25
+ totalIntegrationMinutes: 120,
26
+ };
27
+
28
+ function finiteOr(value: number, fallback: number): number {
29
+ return Number.isFinite(value) ? value : fallback;
30
+ }
31
+
32
+ export function normalizeInput(input: ExposurePlannerInput): ExposurePlannerInput {
33
+ return {
34
+ ...input,
35
+ focalLength: Math.min(600, Math.max(5, finiteOr(input.focalLength, DEFAULT_INPUT.focalLength))),
36
+ aperture: Math.min(32, Math.max(1, finiteOr(input.aperture, DEFAULT_INPUT.aperture))),
37
+ pixelPitch: Math.min(30, Math.max(0.5, finiteOr(input.pixelPitch, DEFAULT_INPUT.pixelPitch))),
38
+ declination: Math.min(89, Math.max(0, finiteOr(input.declination, DEFAULT_INPUT.declination))),
39
+ trackingExposureSeconds: Math.min(900, Math.max(5, finiteOr(input.trackingExposureSeconds, DEFAULT_INPUT.trackingExposureSeconds))),
40
+ deadTimeSeconds: Math.min(120, Math.max(0, finiteOr(input.deadTimeSeconds, DEFAULT_INPUT.deadTimeSeconds))),
41
+ totalIntegrationMinutes: Math.min(1440, Math.max(1, finiteOr(input.totalIntegrationMinutes, DEFAULT_INPUT.totalIntegrationMinutes))),
42
+ };
43
+ }
44
+
45
+ export function calculateRule500(focalLength: number, sensor: SensorPreset): number {
46
+ const cropFactor = SENSOR_CROP_FACTORS[sensor];
47
+ return 500 / (focalLength * cropFactor);
48
+ }
49
+
50
+ export function calculateNpfSeconds(
51
+ focalLength: number,
52
+ aperture: number,
53
+ pixelPitch: number,
54
+ declination: number,
55
+ ): number {
56
+ const declinationRadians = declination * (Math.PI / 180);
57
+ const cosine = Math.max(0.1, Math.cos(declinationRadians));
58
+ return (16.9 * aperture + 0.1 * focalLength + 13.7 * pixelPitch) / (focalLength * cosine);
59
+ }
60
+
61
+ export function calculateTimeline(frameCount: number): TimelineSlice[] {
62
+ const visibleSlices = Math.min(12, Math.max(1, frameCount));
63
+ return Array.from({ length: visibleSlices }, (_, index) => ({
64
+ index: index + 1,
65
+ width: 100 / visibleSlices,
66
+ isFinal: index === visibleSlices - 1,
67
+ }));
68
+ }
69
+
70
+ export function calculateExposurePlan(rawInput: ExposurePlannerInput): ExposurePlannerResult {
71
+ const input = normalizeInput(rawInput);
72
+ const npfSeconds = calculateNpfSeconds(input.focalLength, input.aperture, input.pixelPitch, input.declination);
73
+ const rule500Seconds = calculateRule500(input.focalLength, input.sensor);
74
+ const untrackedCeilingSeconds = input.exposureRule === 'npf' ? npfSeconds : rule500Seconds;
75
+ const selectedCeilingSeconds = input.tracking === 'tracked' ? input.trackingExposureSeconds : untrackedCeilingSeconds;
76
+ const totalSeconds = input.totalIntegrationMinutes * 60;
77
+ const frameCount = Math.ceil(totalSeconds / selectedCeilingSeconds);
78
+ const plannedIntegrationMinutes = (frameCount * selectedCeilingSeconds) / 60;
79
+ const deadTimeMinutes = (frameCount * input.deadTimeSeconds) / 60;
80
+ const stackingGainStops = Math.log2(frameCount) / 2;
81
+ const npfDifferencePercent = ((npfSeconds - rule500Seconds) / rule500Seconds) * 100;
82
+ return {
83
+ npfSeconds,
84
+ rule500Seconds,
85
+ selectedCeilingSeconds,
86
+ frameCount,
87
+ plannedIntegrationMinutes,
88
+ deadTimePerFrameSeconds: input.deadTimeSeconds,
89
+ deadTimeMinutes,
90
+ sessionDurationMinutes: plannedIntegrationMinutes + deadTimeMinutes,
91
+ stackingGainStops,
92
+ npfDifferencePercent,
93
+ timeline: calculateTimeline(frameCount),
94
+ };
95
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { astrophotographyExposureStackPlanner } 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 astrophotographyExposureStackPlanner.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,22 @@
1
+ import type { ExposurePlannerInput } from './ui';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-astronomy-exposure-stack-planner';
4
+
5
+ export function loadExposurePlannerInput(fallback: ExposurePlannerInput): ExposurePlannerInput {
6
+ try {
7
+ const raw = window.localStorage.getItem(STORAGE_KEY);
8
+ if (!raw) return fallback;
9
+ const parsed = JSON.parse(raw) as Partial<ExposurePlannerInput>;
10
+ return { ...fallback, ...parsed };
11
+ } catch {
12
+ return fallback;
13
+ }
14
+ }
15
+
16
+ export function saveExposurePlannerInput(input: ExposurePlannerInput): void {
17
+ try {
18
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(input));
19
+ } catch {
20
+ return;
21
+ }
22
+ }
@@ -0,0 +1,102 @@
1
+ import type { BibliographyEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type ExposureRule = 'npf' | 'rule500';
4
+ export type SensorPreset = 'fullFrame' | 'apsC' | 'microFourThirds' | 'phone';
5
+ export type TrackingMode = 'untracked' | 'tracked';
6
+
7
+ export interface ExposurePlannerInput {
8
+ exposureRule: ExposureRule;
9
+ sensor: SensorPreset;
10
+ focalLength: number;
11
+ aperture: number;
12
+ pixelPitch: number;
13
+ declination: number;
14
+ tracking: TrackingMode;
15
+ trackingExposureSeconds: number;
16
+ deadTimeSeconds: number;
17
+ totalIntegrationMinutes: number;
18
+ }
19
+
20
+ export interface TimelineSlice {
21
+ index: number;
22
+ width: number;
23
+ isFinal: boolean;
24
+ }
25
+
26
+ export interface ExposurePlannerResult {
27
+ npfSeconds: number;
28
+ rule500Seconds: number;
29
+ selectedCeilingSeconds: number;
30
+ frameCount: number;
31
+ plannedIntegrationMinutes: number;
32
+ deadTimePerFrameSeconds: number;
33
+ deadTimeMinutes: number;
34
+ sessionDurationMinutes: number;
35
+ stackingGainStops: number;
36
+ npfDifferencePercent: number;
37
+ timeline: TimelineSlice[];
38
+ }
39
+
40
+ export interface ExposurePlannerUI {
41
+ cameraLegend: string;
42
+ focalLabel: string;
43
+ focalHelp: string;
44
+ apertureLabel: string;
45
+ apertureHelp: string;
46
+ pixelPitchLabel: string;
47
+ pixelPitchHelp: string;
48
+ declinationLabel: string;
49
+ declinationHelp: string;
50
+ sensorLabel: string;
51
+ sensorOptions: Record<SensorPreset, string>;
52
+ ruleLabel: string;
53
+ ruleNpf: string;
54
+ rule500: string;
55
+ trackingLabel: string;
56
+ untracked: string;
57
+ tracked: string;
58
+ trackingExposureLabel: string;
59
+ trackingExposureHelp: string;
60
+ deadTimeLabel: string;
61
+ deadTimeHelp: string;
62
+ totalIntegrationLabel: string;
63
+ totalIntegrationHelp: string;
64
+ resultTitle: string;
65
+ resultKicker: string;
66
+ secondsUnit: string;
67
+ framesLabel: string;
68
+ totalTimeLabel: string;
69
+ sessionTimeLabel: string;
70
+ gainLabel: string;
71
+ sessionDetail: string;
72
+ workloadText: string;
73
+ highWorkloadText: string;
74
+ sensorPixelWarning: string;
75
+ comparisonTitle: string;
76
+ npfLabel: string;
77
+ rule500Label: string;
78
+ deltaLabel: string;
79
+ timelineTitle: string;
80
+ timelineStart: string;
81
+ timelineEnd: string;
82
+ trackingNote: string;
83
+ untrackedNote: string;
84
+ methodTitle: string;
85
+ methodText: string;
86
+ limitsTitle: string;
87
+ limitsText: string;
88
+ startingStatus: string;
89
+ formulaLabel: string;
90
+ formulaNpf: string;
91
+ formula500: string;
92
+ presetsLabel: string;
93
+ presets: Record<string, string>;
94
+ }
95
+
96
+ export type ExposurePlannerLocaleContent = ToolLocaleContent<ExposurePlannerUI>;
97
+
98
+ export interface ExposurePlannerBibliographyEntry extends BibliographyEntry {
99
+ country: string;
100
+ language: string;
101
+ supports: string;
102
+ }
@@ -129,39 +129,61 @@ function calculateDomeCoords(input: ObservingPlannerInput, evaluation: Evaluatio
129
129
  return { peakHour, elev, isAbove, radiantX, radiantY, moonX, moonY };
130
130
  }
131
131
 
132
- function getConstellationName(constKey: string, ui?: MeteorShowerObservingPlannerUI): string {
133
- if (!ui || !ui.constellations) return constKey;
134
- return ui.constellations[constKey] || constKey;
135
- }
136
-
137
132
  function getStatusText(isAbove: boolean, elev: number, ui?: MeteorShowerObservingPlannerUI): string {
138
133
  const alt = ui && ui.altLabel ? ui.altLabel : 'Alt';
139
134
  const below = ui && ui.belowHorizonLabel ? ui.belowHorizonLabel : 'Below Horizon';
140
135
  return isAbove ? `+${elev}° ${alt}` : below;
141
136
  }
142
137
 
143
- export function renderSkyDomeSvg(input: ObservingPlannerInput, evaluation: EvaluationResult, ui?: MeteorShowerObservingPlannerUI): string {
138
+ function getConstellationName(constKey: string, ui?: MeteorShowerObservingPlannerUI): string {
139
+ if (!ui?.constellations) return constKey;
140
+ return ui.constellations[constKey] || constKey;
141
+ }
142
+
143
+ interface DomePresentation {
144
+ coords: DomeCoords;
145
+ peakZhr: number;
146
+ constName: string;
147
+ statusTxt: string;
148
+ moonLabelTxt: string;
149
+ belowHorizonTxt: string;
150
+ telemetryTitle: string;
151
+ }
152
+
153
+ function createDomePresentation(
154
+ input: ObservingPlannerInput,
155
+ evaluation: EvaluationResult,
156
+ ui?: MeteorShowerObservingPlannerUI,
157
+ ): DomePresentation {
144
158
  const shower = getShowerById(input.showerId);
145
159
  const coords = calculateDomeCoords(input, evaluation);
146
160
  const peakItem = evaluation.hourlyBreakdown.find((h) => h.isPeakHour) || evaluation.hourlyBreakdown[0];
161
+ const labels = ui ?? {};
162
+ return {
163
+ coords,
164
+ peakZhr: peakItem?.effectiveZhr ?? 0,
165
+ constName: getConstellationName(shower.radiantConstellation, ui),
166
+ statusTxt: getStatusText(coords.isAbove, coords.elev, ui),
167
+ moonLabelTxt: labels.moonLabel ?? 'Moon',
168
+ belowHorizonTxt: labels.belowHorizonLabel ?? 'Below Horizon',
169
+ telemetryTitle: labels.radiantTelemetryLabel ?? 'RADIANT TELEMETRY',
170
+ };
171
+ }
147
172
 
173
+ export function renderSkyDomeSvg(input: ObservingPlannerInput, evaluation: EvaluationResult, ui?: MeteorShowerObservingPlannerUI): string {
174
+ const presentation = createDomePresentation(input, evaluation, ui);
148
175
  const moonOpacity = (input.moonPhase * 0.5).toFixed(2);
149
- const constName = getConstellationName(shower.radiantConstellation, ui);
150
- const statusTxt = getStatusText(coords.isAbove, coords.elev, ui);
151
- const moonLabelTxt = ui && ui.moonLabel ? ui.moonLabel : 'Moon';
152
- const belowHorizonTxt = ui && ui.belowHorizonLabel ? ui.belowHorizonLabel : 'Below Horizon';
153
- const telemetryTitle = ui && ui.radiantTelemetryLabel ? ui.radiantTelemetryLabel : 'RADIANT TELEMETRY';
154
-
155
- const moonParams: MoonGraphicParams = { moonPhase: input.moonPhase, x: coords.moonX, y: coords.moonY, moonTxt: moonLabelTxt };
176
+ const { coords } = presentation;
177
+ const moonParams: MoonGraphicParams = { moonPhase: input.moonPhase, x: coords.moonX, y: coords.moonY, moonTxt: presentation.moonLabelTxt };
156
178
 
157
179
  return `
158
180
  <svg class="sky-dome-svg" viewBox="0 0 400 230" role="img" aria-label="Celestial Sky Dome">
159
181
  ${renderSkyDomeBackground(moonOpacity)}
160
182
  ${renderSkyDomeGrid()}
161
- ${renderTelemetryCard(telemetryTitle, constName, statusTxt, coords.isAbove ? '#10b981' : '#f43f5e')}
183
+ ${renderTelemetryCard(presentation.telemetryTitle, presentation.constName, presentation.statusTxt, coords.isAbove ? '#10b981' : '#f43f5e')}
162
184
  ${renderMoonGraphic(moonParams)}
163
- ${renderMeteorStreaks(coords.isAbove, peakItem ? peakItem.effectiveZhr : 0, coords.radiantX, coords.radiantY)}
164
- ${renderRadiantMarker(coords.isAbove, coords.radiantX, coords.radiantY, belowHorizonTxt)}
185
+ ${renderMeteorStreaks(coords.isAbove, presentation.peakZhr, coords.radiantX, coords.radiantY)}
186
+ ${renderRadiantMarker(coords.isAbove, coords.radiantX, coords.radiantY, presentation.belowHorizonTxt)}
165
187
  </svg>
166
188
  `;
167
189
  }