@jjlmoya/utils-astronomy 1.27.0 → 1.28.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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -2
  3. package/src/data.ts +2 -1
  4. package/src/entries.ts +4 -1
  5. package/src/tests/locale_completeness.test.ts +2 -3
  6. package/src/tests/tool_validation.test.ts +7 -6
  7. package/src/tool/telescopeExitPupilPlanner/bibliography.astro +14 -0
  8. package/src/tool/telescopeExitPupilPlanner/bibliography.ts +12 -0
  9. package/src/tool/telescopeExitPupilPlanner/component.astro +93 -0
  10. package/src/tool/telescopeExitPupilPlanner/controller.ts +102 -0
  11. package/src/tool/telescopeExitPupilPlanner/dom-views.ts +64 -0
  12. package/src/tool/telescopeExitPupilPlanner/entry.ts +31 -0
  13. package/src/tool/telescopeExitPupilPlanner/evaluator.ts +17 -0
  14. package/src/tool/telescopeExitPupilPlanner/i18n/de.ts +2 -0
  15. package/src/tool/telescopeExitPupilPlanner/i18n/en.ts +177 -0
  16. package/src/tool/telescopeExitPupilPlanner/i18n/es.ts +2 -0
  17. package/src/tool/telescopeExitPupilPlanner/i18n/fr.ts +2 -0
  18. package/src/tool/telescopeExitPupilPlanner/i18n/id.ts +2 -0
  19. package/src/tool/telescopeExitPupilPlanner/i18n/it.ts +2 -0
  20. package/src/tool/telescopeExitPupilPlanner/i18n/ja.ts +2 -0
  21. package/src/tool/telescopeExitPupilPlanner/i18n/ko.ts +2 -0
  22. package/src/tool/telescopeExitPupilPlanner/i18n/nl.ts +2 -0
  23. package/src/tool/telescopeExitPupilPlanner/i18n/pl.ts +2 -0
  24. package/src/tool/telescopeExitPupilPlanner/i18n/pt.ts +2 -0
  25. package/src/tool/telescopeExitPupilPlanner/i18n/ru.ts +2 -0
  26. package/src/tool/telescopeExitPupilPlanner/i18n/sv.ts +2 -0
  27. package/src/tool/telescopeExitPupilPlanner/i18n/tr.ts +2 -0
  28. package/src/tool/telescopeExitPupilPlanner/i18n/translated.ts +140 -0
  29. package/src/tool/telescopeExitPupilPlanner/i18n/zh.ts +2 -0
  30. package/src/tool/telescopeExitPupilPlanner/index.ts +10 -0
  31. package/src/tool/telescopeExitPupilPlanner/logic.test.ts +39 -0
  32. package/src/tool/telescopeExitPupilPlanner/logic.ts +57 -0
  33. package/src/tool/telescopeExitPupilPlanner/seo.astro +15 -0
  34. package/src/tool/telescopeExitPupilPlanner/storage.ts +24 -0
  35. package/src/tool/telescopeExitPupilPlanner/telescope-exit-pupil-magnification-planner.css +454 -0
  36. package/src/tool/telescopeExitPupilPlanner/ui.ts +40 -0
  37. package/src/tools.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-astronomy",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -3,11 +3,12 @@ import { bortleVisualizer } from '../tool/bortleVisualizer/entry';
3
3
  import { deepSpaceScope } from '../tool/deepSpaceScope/entry';
4
4
  import { starExposureCalculator } from '../tool/starExposureCalculator/entry';
5
5
  import { telescopeResolution } from '../tool/telescopeResolution/entry';
6
- import { eyepieceCalculator } from '../tool/smartEyepieceCalculator/entry';
6
+ import { eyepieceCalculator } from '../tool/smartEyepieceCalculator/entry';
7
+ import { telescopeExitPupilPlanner } from '../tool/telescopeExitPupilPlanner/entry';
7
8
 
8
9
  export const toolsCategory: AstronomyCategoryEntry = {
9
10
  icon: 'mdi:telescope',
10
- tools: [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator],
11
+ tools: [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator, telescopeExitPupilPlanner],
11
12
  i18n: {
12
13
  de: () => import('./i18n/de').then((m) => m.content),
13
14
  en: () => import('./i18n/en').then((m) => m.content),
package/src/data.ts CHANGED
@@ -4,7 +4,8 @@ export type { BortleVisualizerUI, BortleVisualizerLocaleContent } from './tool/b
4
4
  export type { DeepSpaceScopeUI, DeepSpaceScopeLocaleContent } from './tool/deepSpaceScope';
5
5
  export type { StarExposureCalculatorUI, StarExposureCalculatorLocaleContent } from './tool/starExposureCalculator';
6
6
  export type { TelescopeResolutionUI, TelescopeResolutionLocaleContent } from './tool/telescopeResolution';
7
- export type { EyepieceCalculatorUI, EyepieceCalculatorLocaleContent } from './tool/smartEyepieceCalculator/entry';
7
+ export type { EyepieceCalculatorUI, EyepieceCalculatorLocaleContent } from './tool/smartEyepieceCalculator/entry';
8
+ export type { TelescopeExitPupilPlannerUI, TelescopeExitPupilPlannerLocaleContent } from './tool/telescopeExitPupilPlanner/entry';
8
9
 
9
10
  export type {
10
11
  KnownLocale,
package/src/entries.ts CHANGED
@@ -8,10 +8,13 @@ export { telescopeResolution } from './tool/telescopeResolution/entry';
8
8
  export type { TelescopeResolutionUI, TelescopeResolutionLocaleContent } from './tool/telescopeResolution/entry';
9
9
  export { eyepieceCalculator } from './tool/smartEyepieceCalculator/entry';
10
10
  export type { EyepieceCalculatorUI, EyepieceCalculatorLocaleContent } from './tool/smartEyepieceCalculator/entry';
11
+ export { telescopeExitPupilPlanner } from './tool/telescopeExitPupilPlanner/entry';
12
+ export type { TelescopeExitPupilPlannerUI, TelescopeExitPupilPlannerLocaleContent } from './tool/telescopeExitPupilPlanner/entry';
11
13
  export { toolsCategory, astronomyCategory } from './category';
12
14
  import { bortleVisualizer } from './tool/bortleVisualizer/entry';
13
15
  import { deepSpaceScope } from './tool/deepSpaceScope/entry';
14
16
  import { starExposureCalculator } from './tool/starExposureCalculator/entry';
15
17
  import { telescopeResolution } from './tool/telescopeResolution/entry';
16
18
  import { eyepieceCalculator } from './tool/smartEyepieceCalculator/entry';
17
- export const ALL_ENTRIES = [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator];
19
+ import { telescopeExitPupilPlanner } from './tool/telescopeExitPupilPlanner/entry';
20
+ export const ALL_ENTRIES = [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator, telescopeExitPupilPlanner];
@@ -2,8 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
3
 
4
4
  describe('Locale Completeness Validation', () => {
5
- it('all 4 tools registered', () => {
6
- expect(ALL_TOOLS.length).toBe(5);
5
+ it('all 6 tools registered', () => {
6
+ expect(ALL_TOOLS.length).toBe(6);
7
7
  });
8
8
  });
9
-
@@ -75,9 +75,10 @@ describe('Tool Validation Suite', () => {
75
75
  'simulador-cielo-oscuro',
76
76
  'alcance-telescopio',
77
77
  'calculadora-regla-500',
78
- 'calculadora-resolucion-telescopio',
79
- 'calculadora-oculares',
80
- ];
78
+ 'calculadora-resolucion-telescopio',
79
+ 'calculadora-oculares',
80
+ 'planificador-pupila-salida-aumentos-telescopio',
81
+ ];
81
82
  expect(validSlugs).toContain(content.slug);
82
83
  }
83
84
  });
@@ -102,12 +103,12 @@ describe('Tool Validation Suite', () => {
102
103
  });
103
104
 
104
105
  describe('Library Registration', () => {
105
- it('should have 5 tools in ALL_TOOLS', () => {
106
- expect(ALL_TOOLS.length).toBe(5);
106
+ it('should have 6 tools in ALL_TOOLS', () => {
107
+ expect(ALL_TOOLS.length).toBe(6);
107
108
  });
108
109
 
109
110
  it('should have all tools in astronomyCategory', () => {
110
- expect(astronomyCategory.tools.length).toBe(5);
111
+ expect(astronomyCategory.tools.length).toBe(6);
111
112
  ALL_TOOLS.forEach(({ entry }) => {
112
113
  const exists = astronomyCategory.tools.some((t: any) => t.id === entry.id);
113
114
  expect(exists).toBe(true);
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { telescopeExitPupilPlanner } 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 telescopeExitPupilPlanner.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Sky & Telescope, Telescope Calculator: magnification, true field and exit pupil',
6
+ url: 'https://skyandtelescope.org/stargazing-and-observing/telescope-calculator/',
7
+ },
8
+ {
9
+ name: 'Universidad Nacional Autónoma de México, Óptica geométrica del telescopio y pupila de salida',
10
+ url: 'https://internos.icat.unam.mx/internosV2/public/documents/materialesRegistrados/documentosPDF/DocumentoPDF_692764f5337f2.pdf',
11
+ },
12
+ ];
@@ -0,0 +1,93 @@
1
+ ---
2
+ import type { TelescopeExitPupilPlannerUI } from './ui';
3
+ import { calculateOptics, DEFAULT_INPUTS } from './logic';
4
+
5
+ interface Props {
6
+ ui: TelescopeExitPupilPlannerUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ const initial = calculateOptics(DEFAULT_INPUTS);
11
+ const uiJson = JSON.stringify(ui).replace(/</g, '\\u003c');
12
+ ---
13
+
14
+ <script is:inline type="application/json" id="tep-ui-data" set:html={uiJson}></script>
15
+
16
+ <div class="tep-root" id="tep-root">
17
+ <div class="tep-layout">
18
+ <section class="tep-inputs" aria-label="Optical setup">
19
+ <div class="tep-input-grid">
20
+ <label class="tep-field">
21
+ <span>{ui.apertureLabel}</span>
22
+ <div class="tep-number-wrap">
23
+ <input data-input="aperture" type="number" min="1" max="2000" step="1" value={DEFAULT_INPUTS.apertureMm} inputmode="decimal" />
24
+ <b>{ui.millimetreUnit}</b>
25
+ </div>
26
+ </label>
27
+ <label class="tep-field">
28
+ <span>{ui.scopeFocalLengthLabel}</span>
29
+ <div class="tep-number-wrap">
30
+ <input data-input="scope-focal-length" type="number" min="1" max="10000" step="1" value={DEFAULT_INPUTS.scopeFocalLengthMm} inputmode="decimal" />
31
+ <b>{ui.millimetreUnit}</b>
32
+ </div>
33
+ </label>
34
+ <label class="tep-field">
35
+ <span>{ui.eyepieceFocalLengthLabel}</span>
36
+ <div class="tep-number-wrap">
37
+ <input data-input="eyepiece-focal-length" type="number" min="1" max="200" step="0.1" value={DEFAULT_INPUTS.eyepieceFocalLengthMm} inputmode="decimal" />
38
+ <b>{ui.millimetreUnit}</b>
39
+ </div>
40
+ </label>
41
+ <div class="tep-field" data-select="barlow">
42
+ <span>{ui.barlowLabel}</span>
43
+ <input data-select-value type="hidden" value={DEFAULT_INPUTS.barlowFactor} />
44
+ <button class="tep-select-trigger" type="button" data-select-trigger aria-expanded="false" aria-haspopup="listbox">{ui.barlowOptions[0].label}</button>
45
+ <div class="tep-select-menu" data-select-menu hidden role="listbox">
46
+ {ui.barlowOptions.map((option) => <button type="button" data-select-option data-value={option.value} aria-selected={option.value === String(DEFAULT_INPUTS.barlowFactor)} role="option">{option.label}</button>)}
47
+ </div>
48
+ </div>
49
+ <div class="tep-field" data-select="apparent-field">
50
+ <span>{ui.apparentFieldLabel}</span>
51
+ <input data-select-value type="hidden" value={DEFAULT_INPUTS.apparentFieldDeg} />
52
+ <button class="tep-select-trigger" type="button" data-select-trigger aria-expanded="false" aria-haspopup="listbox">{ui.apparentFieldOptions[1].label}</button>
53
+ <div class="tep-select-menu" data-select-menu hidden role="listbox">
54
+ {ui.apparentFieldOptions.map((option) => <button type="button" data-select-option data-value={option.value} aria-selected={option.value === String(DEFAULT_INPUTS.apparentFieldDeg)} role="option">{option.label}</button>)}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <p class="tep-hint">{ui.presetHint}</p>
59
+ <button class="tep-reset" type="button" data-reset>{ui.resetLabel}</button>
60
+ </section>
61
+
62
+ <section class="tep-output" aria-live="polite">
63
+ <div class="tep-primary-result">
64
+ <span>{ui.magnificationLabel}</span>
65
+ <strong data-result="magnification">{initial.magnification.toFixed(1)}</strong><b>{ui.magnificationUnit}</b>
66
+ <small>{ui.magnificationHint}</small>
67
+ </div>
68
+ <div class="tep-metrics">
69
+ <div><span>{ui.exitPupilLabel}</span><div class="tep-metric-value"><strong data-result="exit-pupil">{initial.exitPupilMm.toFixed(2)}</strong><b>{ui.exitPupilUnit}</b></div></div>
70
+ <div><span>{ui.trueFieldLabel}</span><div class="tep-metric-value"><strong data-result="true-field">{initial.trueFieldDeg.toFixed(2)}</strong><b>{ui.fieldUnit}</b></div></div>
71
+ <div><span>{ui.focalRatioLabel}</span><div class="tep-metric-value"><strong data-result="focal-ratio">f/{initial.focalRatio.toFixed(1)}</strong><b>{ui.focalRatioUnit}</b></div></div>
72
+ </div>
73
+ <div class="tep-scene-wrap">
74
+ <div class="tep-scene-heading"><span>{ui.opticalPathLabel}</span><strong data-result="pupil-status">{ui.pupilWorking}</strong></div>
75
+ <div data-scene class="tep-scene" aria-label={ui.opticalPathLabel}></div>
76
+ <p class="tep-scene-copy" data-result="pupil-detail">{ui.pupilWorkingDetail}</p>
77
+ </div>
78
+ <div class="tep-footnotes">
79
+ <p><span>{ui.effectiveEyepieceLabel}</span><strong data-result="effective-eyepiece">{initial.effectiveEyepieceMm.toFixed(2)} {ui.millimetreUnit}</strong></p>
80
+ <p><span>{ui.assumptionLabel}</span>{ui.assumptionText}</p>
81
+ <p class="tep-hint">{ui.trueFieldHint}</p>
82
+ </div>
83
+ </section>
84
+ </div>
85
+ </div>
86
+
87
+ <script>
88
+ import { mountTelescopeExitPupilPlanner } from './controller';
89
+
90
+ const root = document.getElementById('tep-root');
91
+ const data = document.getElementById('tep-ui-data');
92
+ if (root && data) mountTelescopeExitPupilPlanner(root, JSON.parse(data.textContent || '{}'));
93
+ </script>
@@ -0,0 +1,102 @@
1
+ import { calculateOptics, DEFAULT_INPUTS, type OpticsInputs } from './logic';
2
+ import { clearStoredInputs, readStoredInputs, writeStoredInputs } from './storage';
3
+ import { renderResults } from './dom-views';
4
+ import type { TelescopeExitPupilPlannerUI } from './ui';
5
+
6
+ interface CustomSelect {
7
+ root: HTMLElement;
8
+ value: HTMLInputElement;
9
+ trigger: HTMLButtonElement;
10
+ menu: HTMLElement;
11
+ }
12
+
13
+ function numberInput(root: HTMLElement, name: string): HTMLInputElement {
14
+ return root.querySelector<HTMLInputElement>(`[data-input="${name}"]`) as HTMLInputElement;
15
+ }
16
+
17
+ function setupSelect(root: HTMLElement): CustomSelect {
18
+ const value = root.querySelector<HTMLInputElement>('[data-select-value]') as HTMLInputElement;
19
+ const trigger = root.querySelector<HTMLButtonElement>('[data-select-trigger]') as HTMLButtonElement;
20
+ const menu = root.querySelector<HTMLElement>('[data-select-menu]') as HTMLElement;
21
+ const options = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-select-option]'));
22
+
23
+ const close = () => {
24
+ menu.hidden = true;
25
+ trigger.setAttribute('aria-expanded', 'false');
26
+ };
27
+ const open = () => {
28
+ menu.hidden = false;
29
+ trigger.setAttribute('aria-expanded', 'true');
30
+ };
31
+ trigger.addEventListener('click', () => (menu.hidden ? open() : close()));
32
+ options.forEach((option) => {
33
+ option.addEventListener('click', () => {
34
+ value.value = option.dataset.value || '';
35
+ trigger.textContent = option.textContent || '';
36
+ options.forEach((item) => item.setAttribute('aria-selected', String(item === option)));
37
+ close();
38
+ root.dispatchEvent(new Event('change', { bubbles: true }));
39
+ });
40
+ });
41
+ document.addEventListener('click', (event) => {
42
+ if (!root.contains(event.target as Node)) close();
43
+ });
44
+ return { root, value, trigger, menu };
45
+ }
46
+
47
+ function readInputs(root: HTMLElement, selects: CustomSelect[]): OpticsInputs {
48
+ const value = (name: string) => Number(numberInput(root, name).value);
49
+ const selectValue = (name: string) => Number(selects.find((select) => select.root.dataset.select === name)?.value.value);
50
+ return {
51
+ apertureMm: value('aperture'),
52
+ scopeFocalLengthMm: value('scope-focal-length'),
53
+ eyepieceFocalLengthMm: value('eyepiece-focal-length'),
54
+ barlowFactor: selectValue('barlow'),
55
+ apparentFieldDeg: selectValue('apparent-field'),
56
+ };
57
+ }
58
+
59
+ function writeInputs(root: HTMLElement, selects: CustomSelect[], inputs: OpticsInputs): void {
60
+ numberInput(root, 'aperture').value = String(inputs.apertureMm);
61
+ numberInput(root, 'scope-focal-length').value = String(inputs.scopeFocalLengthMm);
62
+ numberInput(root, 'eyepiece-focal-length').value = String(inputs.eyepieceFocalLengthMm);
63
+ selects.forEach((select) => {
64
+ const inputName = select.root.dataset.select;
65
+ const value = inputName === 'barlow' ? inputs.barlowFactor : inputs.apparentFieldDeg;
66
+ const option = select.root.querySelector<HTMLButtonElement>(`[data-select-option][data-value="${value}"]`);
67
+ if (option) {
68
+ select.value.value = String(value);
69
+ select.trigger.textContent = option.textContent || '';
70
+ select.root.querySelectorAll('[data-select-option]').forEach((item) => item.setAttribute('aria-selected', String(item === option)));
71
+ }
72
+ });
73
+ }
74
+
75
+ function bindReset(root: HTMLElement, selects: CustomSelect[], ui: TelescopeExitPupilPlannerUI): void {
76
+ const reset = root.querySelector<HTMLButtonElement>('[data-reset]');
77
+ reset?.addEventListener('click', () => {
78
+ clearStoredInputs();
79
+ writeInputs(root, selects, DEFAULT_INPUTS);
80
+ root.dispatchEvent(new Event('input', { bubbles: true }));
81
+ reset.textContent = ui.resetLabel;
82
+ });
83
+ }
84
+
85
+ export function mountTelescopeExitPupilPlanner(root: HTMLElement, ui: TelescopeExitPupilPlannerUI): void {
86
+ const selects = Array.from(root.querySelectorAll<HTMLElement>('[data-select]')).map(setupSelect);
87
+ const stored = readStoredInputs();
88
+ const inputs = { ...DEFAULT_INPUTS, ...stored };
89
+ writeInputs(root, selects, inputs);
90
+
91
+ const update = () => {
92
+ const nextInputs = readInputs(root, selects);
93
+ const result = calculateOptics(nextInputs);
94
+ renderResults(root, result, ui);
95
+ writeStoredInputs(nextInputs);
96
+ };
97
+
98
+ root.querySelectorAll('input').forEach((input) => input.addEventListener('input', update));
99
+ root.querySelectorAll('[data-select]').forEach((select) => select.addEventListener('change', update));
100
+ bindReset(root, selects, ui);
101
+ update();
102
+ }
@@ -0,0 +1,64 @@
1
+ import type { TelescopeExitPupilPlannerUI } from './ui';
2
+ import type { OpticsResult } from './logic';
3
+ import { evaluateExitPupil } from './evaluator';
4
+
5
+ function formatNumber(value: number, digits = 2): string {
6
+ return value.toFixed(digits).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
7
+ }
8
+
9
+ function sceneSvg(result: OpticsResult): string {
10
+ const pupilRadius = 9 + Math.min(22, Math.max(2, result.exitPupilMm * 3));
11
+ const coneWidth = 18 + Math.min(72, result.exitPupilMm * 10);
12
+ const fieldRadius = 34 + Math.min(42, result.trueFieldDeg * 16);
13
+
14
+ return `<svg class="tep-optical-scene" viewBox="0 0 640 250" role="img" aria-label="Optical path from aperture to exit pupil">
15
+ <rect class="tep-scene-ground" x="0" y="0" width="640" height="250" rx="18" />
16
+ <path class="tep-light-cone" d="M 92 87 L 420 ${125 - coneWidth / 2} L 420 ${125 + coneWidth / 2} Z" />
17
+ <path class="tep-light-cone tep-light-cone-secondary" d="M 92 163 L 420 ${125 - coneWidth / 2} L 420 ${125 + coneWidth / 2} Z" />
18
+ <rect class="tep-tube" x="42" y="76" width="58" height="98" rx="12" />
19
+ <circle class="tep-aperture" cx="71" cy="125" r="35" />
20
+ <circle class="tep-aperture-core" cx="71" cy="125" r="19" />
21
+ <rect class="tep-eyepiece" x="408" y="77" width="42" height="96" rx="10" />
22
+ <circle class="tep-exit-ring" cx="450" cy="125" r="${pupilRadius + 9}" />
23
+ <circle class="tep-exit-pupil" cx="450" cy="125" r="${pupilRadius}" />
24
+ <path class="tep-eye" d="M 500 125 Q 550 69 600 125 Q 550 181 500 125 Z" />
25
+ <circle class="tep-eye-iris" cx="550" cy="125" r="${Math.min(18, pupilRadius)}" />
26
+ <circle class="tep-eye-glint" cx="556" cy="118" r="3" />
27
+ <line class="tep-guide" x1="71" y1="207" x2="450" y2="207" />
28
+ <text class="tep-scene-label" x="71" y="232" text-anchor="middle">aperture</text>
29
+ <text class="tep-scene-label" x="450" y="232" text-anchor="middle">exit pupil</text>
30
+ <text class="tep-scene-value" x="550" y="49" text-anchor="middle">${formatNumber(result.trueFieldDeg, 2)}° field</text>
31
+ <circle class="tep-field-orbit" cx="550" cy="125" r="${fieldRadius}" />
32
+ </svg>`;
33
+ }
34
+
35
+ function setText(root: HTMLElement, name: string, value: string): void {
36
+ const element = root.querySelector<HTMLElement>(`[data-result="${name}"]`);
37
+ if (element) element.textContent = value;
38
+ }
39
+
40
+ function updateEvaluation(root: HTMLElement, result: OpticsResult, ui: TelescopeExitPupilPlannerUI): void {
41
+ const evaluation = evaluateExitPupil(result);
42
+ const labels = {
43
+ 'too-small': [ui.pupilTooSmall, ui.pupilTooSmallDetail],
44
+ detail: [ui.pupilDetail, ui.pupilDetailDetail],
45
+ working: [ui.pupilWorking, ui.pupilWorkingDetail],
46
+ wide: [ui.pupilWide, ui.pupilWideDetail],
47
+ } as const;
48
+ const [label, detail] = labels[evaluation.band];
49
+ setText(root, 'pupil-status', label);
50
+ setText(root, 'pupil-detail', detail);
51
+ const marker = root.querySelector<HTMLElement>('[data-result="pupil-marker"]');
52
+ if (marker) marker.style.left = `${evaluation.markerPercent}%`;
53
+ const scene = root.querySelector<HTMLElement>('[data-scene]');
54
+ if (scene) scene.innerHTML = sceneSvg(result);
55
+ }
56
+
57
+ export function renderResults(root: HTMLElement, result: OpticsResult, ui: TelescopeExitPupilPlannerUI): void {
58
+ setText(root, 'magnification', formatNumber(result.magnification, 1));
59
+ setText(root, 'exit-pupil', formatNumber(result.exitPupilMm, 2));
60
+ setText(root, 'true-field', formatNumber(result.trueFieldDeg, 2));
61
+ setText(root, 'focal-ratio', `f/${formatNumber(result.focalRatio, 1)}`);
62
+ setText(root, 'effective-eyepiece', `${formatNumber(result.effectiveEyepieceMm, 2)} ${ui.millimetreUnit}`);
63
+ updateEvaluation(root, result, ui);
64
+ }
@@ -0,0 +1,31 @@
1
+ import type { AstronomyToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { TelescopeExitPupilPlannerUI } from './ui';
3
+
4
+ export type TelescopeExitPupilPlannerLocaleContent = ToolLocaleContent<TelescopeExitPupilPlannerUI>;
5
+
6
+ export const telescopeExitPupilPlanner: AstronomyToolEntry<TelescopeExitPupilPlannerUI> = {
7
+ id: 'telescope-exit-pupil-planner',
8
+ icons: {
9
+ bg: 'mdi:eye-outline',
10
+ fg: 'mdi:telescope',
11
+ },
12
+ i18n: {
13
+ de: () => import('./i18n/de').then((module) => module.content),
14
+ en: () => import('./i18n/en').then((module) => module.content),
15
+ es: () => import('./i18n/es').then((module) => module.content),
16
+ fr: () => import('./i18n/fr').then((module) => module.content),
17
+ id: () => import('./i18n/id').then((module) => module.content),
18
+ it: () => import('./i18n/it').then((module) => module.content),
19
+ ja: () => import('./i18n/ja').then((module) => module.content),
20
+ ko: () => import('./i18n/ko').then((module) => module.content),
21
+ nl: () => import('./i18n/nl').then((module) => module.content),
22
+ pl: () => import('./i18n/pl').then((module) => module.content),
23
+ pt: () => import('./i18n/pt').then((module) => module.content),
24
+ ru: () => import('./i18n/ru').then((module) => module.content),
25
+ sv: () => import('./i18n/sv').then((module) => module.content),
26
+ tr: () => import('./i18n/tr').then((module) => module.content),
27
+ zh: () => import('./i18n/zh').then((module) => module.content),
28
+ },
29
+ };
30
+
31
+ export { bibliography } from './bibliography';
@@ -0,0 +1,17 @@
1
+ import type { OpticsResult } from './logic';
2
+
3
+ export type PupilBand = 'too-small' | 'detail' | 'working' | 'wide';
4
+
5
+ export interface PupilEvaluation {
6
+ band: PupilBand;
7
+ markerPercent: number;
8
+ }
9
+
10
+ export function evaluateExitPupil(result: OpticsResult): PupilEvaluation {
11
+ const markerPercent = Math.min(100, Math.max(0, (result.exitPupilMm / 7) * 100));
12
+
13
+ if (result.exitPupilMm < 0.5) return { band: 'too-small', markerPercent };
14
+ if (result.exitPupilMm < 1) return { band: 'detail', markerPercent };
15
+ if (result.exitPupilMm <= 5) return { band: 'working', markerPercent };
16
+ return { band: 'wide', markerPercent };
17
+ }
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('de');
@@ -0,0 +1,177 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { TelescopeExitPupilPlannerLocaleContent } from '../entry';
4
+ import type { TelescopeExitPupilPlannerUI } from '../ui';
5
+
6
+ const slug = 'telescope-exit-pupil-magnification-planner';
7
+ const title = 'Telescope Exit Pupil and Magnification Planner';
8
+ const description = 'Plan a telescope and eyepiece combination by calculating magnification, exit pupil, focal ratio and an estimated true field of view.';
9
+
10
+ const ui: TelescopeExitPupilPlannerUI = {
11
+ apertureLabel: 'Telescope aperture',
12
+ scopeFocalLengthLabel: 'Telescope focal length',
13
+ eyepieceFocalLengthLabel: 'Eyepiece focal length',
14
+ barlowLabel: 'Barlow factor',
15
+ apparentFieldLabel: 'Eyepiece apparent field',
16
+ millimetreUnit: 'mm',
17
+ presetHint: 'Use the figures printed on your telescope and eyepiece. The field estimate assumes the selected apparent field.',
18
+ barlowOptions: [
19
+ { value: '1', label: 'No Barlow, 1x' },
20
+ { value: '1.5', label: '1.5x Barlow' },
21
+ { value: '2', label: '2x Barlow' },
22
+ { value: '2.5', label: '2.5x Barlow' },
23
+ { value: '3', label: '3x Barlow' },
24
+ ],
25
+ apparentFieldOptions: [
26
+ { value: '40', label: '40° narrow field' },
27
+ { value: '50', label: '50° standard field' },
28
+ { value: '60', label: '60° wide field' },
29
+ { value: '68', label: '68° wide angle' },
30
+ { value: '82', label: '82° ultra wide field' },
31
+ ],
32
+ magnificationLabel: 'Magnification',
33
+ exitPupilLabel: 'Exit pupil',
34
+ trueFieldLabel: 'Estimated true field',
35
+ focalRatioLabel: 'Focal ratio',
36
+ effectiveEyepieceLabel: 'Effective eyepiece',
37
+ magnificationUnit: 'x',
38
+ exitPupilUnit: 'mm',
39
+ fieldUnit: '°',
40
+ focalRatioUnit: 'f ratio',
41
+ opticalPathLabel: 'The optical path',
42
+ opticalPathDescription: 'A wider beam is brighter and easier to place your eye into. A narrower beam supports higher detail but gives a dimmer, less forgiving view.',
43
+ pupilTooSmall: 'Very demanding pupil',
44
+ pupilDetail: 'High detail pupil',
45
+ pupilWorking: 'Comfortable working pupil',
46
+ pupilWide: 'Wide field pupil',
47
+ pupilTooSmallDetail: 'Below 0.5 mm, diffraction, floaters and atmospheric turbulence become especially intrusive.',
48
+ pupilDetailDetail: 'Between 0.5 and 1 mm, the view favours fine detail when the atmosphere and optics cooperate.',
49
+ pupilWorkingDetail: 'Between 1 and 5 mm, the view balances brightness, detail and comfortable eye placement.',
50
+ pupilWideDetail: 'Above 5 mm, the view favours brightness and framing. Your eye may accept only part of a very large beam.',
51
+ assumptionLabel: 'Field estimate',
52
+ assumptionText: 'True field is estimated as apparent field divided by magnification. Distortion and the eyepiece field stop can make the real sky area differ.',
53
+ magnificationHint: 'Scope focal length divided by effective eyepiece focal length',
54
+ trueFieldHint: 'A planning estimate, not a drift measurement',
55
+ resetLabel: 'Reset figures',
56
+ };
57
+
58
+ const faq: TelescopeExitPupilPlannerLocaleContent['faq'] = [
59
+ {
60
+ question: 'How is telescope magnification calculated?',
61
+ answer: 'Magnification is the telescope focal length divided by the eyepiece focal length. A Barlow lens increases the effective telescope focal length, so this planner multiplies the basic magnification by the selected Barlow factor. For example, a 750 mm telescope with a 25 mm eyepiece gives 30x without a Barlow and 60x with a 2x Barlow.',
62
+ },
63
+ {
64
+ question: 'What does exit pupil tell me?',
65
+ answer: 'Exit pupil is the diameter of the light beam leaving the eyepiece. It equals the aperture divided by magnification, or the eyepiece focal length divided by the telescope focal ratio. A larger exit pupil usually gives a brighter and more forgiving view, while a smaller exit pupil supports higher magnification but makes dimness, eye floaters and unsteady air more noticeable.',
66
+ },
67
+ {
68
+ question: 'Can this planner calculate the exact true field of view?',
69
+ answer: 'No. It gives a useful first estimate by dividing the eyepiece apparent field by magnification. The exact field also depends on the eyepiece field stop and optical distortion. If you need an observed value, measure a star drift or use the manufacturer field stop specification for the specific eyepiece.',
70
+ },
71
+ {
72
+ question: 'Why does a Barlow change the exit pupil?',
73
+ answer: 'A Barlow increases the effective focal length of the telescope while the eyepiece stays the same. That raises magnification and therefore makes the exit pupil smaller. It can provide higher power without requiring an extremely short eyepiece, but the result is still limited by optical quality, alignment, tracking and atmospheric seeing.',
74
+ },
75
+ ];
76
+
77
+ const howTo: TelescopeExitPupilPlannerLocaleContent['howTo'] = [
78
+ { name: 'Enter the telescope aperture', text: 'Enter the clear aperture printed in the telescope specifications. Use millimetres for all figures so the formulas share one unit.' },
79
+ { name: 'Enter both focal lengths', text: 'Add the telescope focal length and the eyepiece focal length. These two numbers determine the base magnification.' },
80
+ { name: 'Choose the optical accessories', text: 'Select the Barlow factor if one is in the optical path, then select the eyepiece apparent field from its specification.' },
81
+ { name: 'Read the optical path', text: 'Use the exit pupil band to decide whether the combination prioritises a wide bright view, a balanced view or high detail. Treat the true field as an estimate.' },
82
+ ];
83
+
84
+ const seo: TelescopeExitPupilPlannerLocaleContent['seo'] = [
85
+ { type: 'title', text: 'Choose an Eyepiece With the Right Optical Tradeoff', level: 2 },
86
+ {
87
+ type: 'paragraph',
88
+ html: 'Choosing an eyepiece is not just a matter of chasing the largest number followed by an x. The same telescope can produce a bright, easy to frame view or a narrow, demanding view depending on the eyepiece and any Barlow lens in the path. This planner turns the specifications on your equipment into four decisions you can compare: magnification, exit pupil, estimated true field and focal ratio.',
89
+ },
90
+ {
91
+ type: 'paragraph',
92
+ html: 'Start with the clear aperture and focal length of the telescope, then enter the focal length printed on the eyepiece. If you use a Barlow, its factor changes the effective focal length of the telescope. The planner shows that change directly, so you can see why a 2x Barlow makes a 25 mm eyepiece behave like a 12.5 mm eyepiece for magnification while preserving the longer eyepiece body.',
93
+ },
94
+ { type: 'title', text: 'How Magnification and Exit Pupil Work Together', level: 2 },
95
+ {
96
+ type: 'paragraph',
97
+ html: 'Magnification is calculated as telescope focal length divided by the effective eyepiece focal length. Exit pupil is aperture divided by magnification. The two values move in opposite directions: pushing power higher shrinks the outgoing light beam. A large exit pupil tends to be comfortable for locating objects and framing extended targets, while a small exit pupil can make fine planetary or double star detail easier to inspect when the optics and atmosphere are steady.',
98
+ },
99
+ {
100
+ type: 'list',
101
+ items: [
102
+ 'Use a wide exit pupil when you need brightness, generous eye placement or a large sweep of sky.',
103
+ 'Use a middle exit pupil when you want a practical balance for general observing.',
104
+ 'Use a small exit pupil only when the target, tracking and seeing justify the extra demand.',
105
+ 'Compare the estimated field with the size of the target before choosing more power.',
106
+ ],
107
+ },
108
+ { type: 'title', text: 'Reading the Field of View Estimate', level: 2 },
109
+ {
110
+ type: 'paragraph',
111
+ html: 'The true field estimate divides the eyepiece apparent field by magnification. It is useful for comparing combinations, but it is not an exact measurement of the sky. The field stop inside the eyepiece and optical distortion can change the result. If the manufacturer publishes a field stop, use it for a more precise calculation. For a direct observing check, time how long a star near the celestial equator takes to drift across the field with the drive turned off.',
112
+ },
113
+ {
114
+ type: 'table',
115
+ headers: ['Combination', 'Magnification', 'Exit pupil', 'Estimated field'],
116
+ rows: [
117
+ ['150 mm aperture, 750 mm scope, 25 mm eyepiece', '30x', '5.0 mm', '1.67° at 50° apparent field'],
118
+ ['150 mm aperture, 750 mm scope, 10 mm eyepiece', '75x', '2.0 mm', '0.67° at 50° apparent field'],
119
+ ['150 mm aperture, 750 mm scope, 25 mm eyepiece with 2x Barlow', '60x', '2.5 mm', '0.83° at 50° apparent field'],
120
+ ],
121
+ },
122
+ { type: 'title', text: 'Limits That the Numbers Cannot See', level: 2 },
123
+ {
124
+ type: 'paragraph',
125
+ html: 'The calculator cannot inspect your eyepiece field stop, optical quality, collimation, mount stability or atmospheric seeing. A mathematically attractive combination may still look soft or shake at the eyepiece. The focal ratio is included as context because it helps you compare instruments, but it does not by itself predict visual image quality. Consider the result a planning map for your equipment, not a promise of resolution or brightness.',
126
+ },
127
+ {
128
+ type: 'tip',
129
+ title: 'Compare Before You Buy',
130
+ html: 'Enter two eyepieces in turn and compare exit pupil and estimated field before ordering an accessory. If the second option only raises magnification while producing a very small pupil and a narrow field, it may be better suited to rare steady nights than to everyday observing.',
131
+ },
132
+ ];
133
+
134
+ const faqSchema: WithContext<FAQPage> = {
135
+ '@context': 'https://schema.org',
136
+ '@type': 'FAQPage',
137
+ mainEntity: faq.map((item) => ({
138
+ '@type': 'Question',
139
+ name: item.question,
140
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
141
+ })),
142
+ };
143
+
144
+ const howToSchema: WithContext<HowTo> = {
145
+ '@context': 'https://schema.org',
146
+ '@type': 'HowTo',
147
+ name: title,
148
+ description,
149
+ step: howTo.map((step) => ({
150
+ '@type': 'HowToStep',
151
+ name: step.name,
152
+ text: step.text,
153
+ })),
154
+ };
155
+
156
+ const appSchema: WithContext<SoftwareApplication> = {
157
+ '@context': 'https://schema.org',
158
+ '@type': 'SoftwareApplication',
159
+ name: title,
160
+ description,
161
+ applicationCategory: 'UtilitiesApplication',
162
+ operatingSystem: 'Web',
163
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
164
+ inLanguage: 'en',
165
+ };
166
+
167
+ export const content: TelescopeExitPupilPlannerLocaleContent = {
168
+ slug,
169
+ title,
170
+ description,
171
+ ui,
172
+ seo,
173
+ faq,
174
+ bibliography,
175
+ howTo,
176
+ schemas: [faqSchema, howToSchema, appSchema],
177
+ };
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('es');
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('fr');
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('id');
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('it');
@@ -0,0 +1,2 @@
1
+ import { createTranslatedContent } from './translated';
2
+ export const content = createTranslatedContent('ja');