@jjlmoya/utils-nature 1.19.0 → 1.21.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/spanish_leakage.test.ts +25 -3
  6. package/src/tests/tool_validation.test.ts +2 -3
  7. package/src/tool/urbanGardenPlanner/bibliography.astro +9 -0
  8. package/src/tool/urbanGardenPlanner/bibliography.ts +16 -0
  9. package/src/tool/urbanGardenPlanner/component.astro +131 -0
  10. package/src/tool/urbanGardenPlanner/controller.ts +88 -0
  11. package/src/tool/urbanGardenPlanner/dom-views.ts +84 -0
  12. package/src/tool/urbanGardenPlanner/entry.ts +29 -0
  13. package/src/tool/urbanGardenPlanner/evaluator.ts +27 -0
  14. package/src/tool/urbanGardenPlanner/i18n/de.ts +50 -0
  15. package/src/tool/urbanGardenPlanner/i18n/en.ts +179 -0
  16. package/src/tool/urbanGardenPlanner/i18n/es.ts +50 -0
  17. package/src/tool/urbanGardenPlanner/i18n/fr.ts +50 -0
  18. package/src/tool/urbanGardenPlanner/i18n/id.ts +50 -0
  19. package/src/tool/urbanGardenPlanner/i18n/it.ts +45 -0
  20. package/src/tool/urbanGardenPlanner/i18n/ja.ts +39 -0
  21. package/src/tool/urbanGardenPlanner/i18n/ko.ts +39 -0
  22. package/src/tool/urbanGardenPlanner/i18n/nl.ts +39 -0
  23. package/src/tool/urbanGardenPlanner/i18n/pl.ts +39 -0
  24. package/src/tool/urbanGardenPlanner/i18n/pt.ts +39 -0
  25. package/src/tool/urbanGardenPlanner/i18n/ru.ts +39 -0
  26. package/src/tool/urbanGardenPlanner/i18n/sv.ts +39 -0
  27. package/src/tool/urbanGardenPlanner/i18n/tr.ts +104 -0
  28. package/src/tool/urbanGardenPlanner/i18n/zh.ts +104 -0
  29. package/src/tool/urbanGardenPlanner/index.ts +11 -0
  30. package/src/tool/urbanGardenPlanner/logic.test.ts +45 -0
  31. package/src/tool/urbanGardenPlanner/logic.ts +55 -0
  32. package/src/tool/urbanGardenPlanner/seo.astro +15 -0
  33. package/src/tool/urbanGardenPlanner/storage.ts +25 -0
  34. package/src/tool/urbanGardenPlanner/ui.ts +38 -0
  35. package/src/tool/urbanGardenPlanner/urban-garden-rainwater-and-soil-planner.css +531 -0
  36. package/src/tools.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-nature",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -3,10 +3,11 @@ import { cricketThermometer } from '../tool/cricketThermometer/entry';
3
3
  import { seedCalculator } from '../tool/seedCalculator/entry';
4
4
  import { rainHarvester } from '../tool/rainHarvester/entry';
5
5
  import { digitalCarbon } from '../tool/digitalCarbon/entry';
6
+ import { urbanGardenPlanner } from '../tool/urbanGardenPlanner/entry';
6
7
 
7
8
  export const natureCategory: NatureCategoryEntry = {
8
9
  icon: 'mdi:leaf',
9
- tools: [cricketThermometer, seedCalculator, rainHarvester, digitalCarbon],
10
+ tools: [cricketThermometer, seedCalculator, rainHarvester, digitalCarbon, urbanGardenPlanner],
10
11
  i18n: {
11
12
  en: () => import('./i18n/en').then((m) => m.content),
12
13
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -6,9 +6,12 @@ export { rainHarvester } from './tool/rainHarvester/entry';
6
6
  export type { RainHarvesterLocaleContent } from './tool/rainHarvester/entry';
7
7
  export { seedCalculator } from './tool/seedCalculator/entry';
8
8
  export type { SeedCalculatorLocaleContent } from './tool/seedCalculator/entry';
9
+ export { urbanGardenPlanner } from './tool/urbanGardenPlanner/entry';
10
+ export type { UrbanGardenPlannerLocaleContent } from './tool/urbanGardenPlanner/entry';
9
11
  export { natureCategory } from './category';
10
12
  import { cricketThermometer } from './tool/cricketThermometer/entry';
11
13
  import { digitalCarbon } from './tool/digitalCarbon/entry';
12
14
  import { rainHarvester } from './tool/rainHarvester/entry';
13
15
  import { seedCalculator } from './tool/seedCalculator/entry';
14
- export const ALL_ENTRIES = [cricketThermometer, digitalCarbon, rainHarvester, seedCalculator];
16
+ import { urbanGardenPlanner } from './tool/urbanGardenPlanner/entry';
17
+ export const ALL_ENTRIES = [cricketThermometer, digitalCarbon, rainHarvester, seedCalculator, urbanGardenPlanner];
package/src/index.ts CHANGED
@@ -24,3 +24,5 @@ export { SEED_CALCULATOR_TOOL, seedCalculator } from './tool/seedCalculator/inde
24
24
  export { RAIN_HARVESTER_TOOL, rainHarvester } from './tool/rainHarvester/index';
25
25
 
26
26
  export { DIGITAL_CARBON_TOOL, digitalCarbon } from './tool/digitalCarbon/index';
27
+
28
+ export { URBAN_GARDEN_PLANNER_TOOL, urbanGardenPlanner } from './tool/urbanGardenPlanner/index';
@@ -28,6 +28,8 @@ const TRANSLATABLE_KEYS = [
28
28
  'schemas',
29
29
  ] as const;
30
30
 
31
+ const COPY_THRESHOLD = 0.9;
32
+
31
33
  const SPANISH_MARKERS = [
32
34
  ['sangre', /\bsangre\b/gi],
33
35
  ['molino', /\bmolino\b/gi],
@@ -65,12 +67,16 @@ function normalize(value: string): string {
65
67
  }
66
68
 
67
69
  function isTechnicalInvariant(text: string): boolean {
70
+ const ledUnitMatches = text.match(/\b\d+(?:-\d+)?\s*(?:w|lm)\b/g) ?? [];
68
71
  return [
69
72
  'data:image/svg+xml;base64',
70
73
  'background-image: url',
71
74
  '.layout-playground {',
72
75
  'const samplerate =',
73
- ].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
76
+ ].some((pattern) => text.includes(pattern)) ||
77
+ (text.includes('presets') && text.includes('hz')) ||
78
+ (text.includes('t_rectal') && text.includes('exp(-k * t)')) ||
79
+ (text.includes('led') && ledUnitMatches.length >= 3);
74
80
  }
75
81
 
76
82
  function collectString(value: string, output: string[]): void {
@@ -111,10 +117,26 @@ function findSpanishMarkers(text: string[]): string[] {
111
117
  );
112
118
  }
113
119
 
120
+ function similarity(left: string, right: string): number {
121
+ const leftTokens = left.split(/\s+/);
122
+ const rightCounts = new Map<string, number>();
123
+ right.split(/\s+/).forEach((token) => rightCounts.set(token, (rightCounts.get(token) ?? 0) + 1));
124
+ const matches = leftTokens.reduce((total, token) => {
125
+ const count = rightCounts.get(token) ?? 0;
126
+ if (count > 0) rightCounts.set(token, count - 1);
127
+ return total + (count > 0 ? 1 : 0);
128
+ }, 0);
129
+ return (2 * matches) / (leftTokens.length + right.split(/\s+/).length);
130
+ }
131
+
114
132
  function findCopiedFragments(spanish: string[], translated: string[]): string[] {
115
- const corpus = translated.join(' ');
116
133
  return spanish
117
- .filter((fragment) => fragment.length >= 80 && corpus.includes(fragment))
134
+ .filter((fragment) => fragment.length >= 80)
135
+ .filter((fragment) => translated.some((candidate) =>
136
+ candidate.length >= 80 &&
137
+ Math.min(fragment.length, candidate.length) / Math.max(fragment.length, candidate.length) >= COPY_THRESHOLD &&
138
+ similarity(fragment, candidate) >= COPY_THRESHOLD,
139
+ ))
118
140
  .sort((a, b) => b.length - a.length)
119
141
  .slice(0, 3);
120
142
  }
@@ -3,8 +3,8 @@ import { ALL_TOOLS, natureCategory } from '../index';
3
3
 
4
4
  describe('Tool Validation Suite', () => {
5
5
  describe('Library Registration', () => {
6
- it('should have 4 tools in ALL_TOOLS', () => {
7
- expect(ALL_TOOLS.length).toBe(4);
6
+ it('should have 5 tools in ALL_TOOLS', () => {
7
+ expect(ALL_TOOLS.length).toBe(5);
8
8
  });
9
9
 
10
10
  it('natureCategory should be defined', () => {
@@ -13,4 +13,3 @@ describe('Tool Validation Suite', () => {
13
13
  });
14
14
  });
15
15
  });
16
-
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { urbanGardenPlanner } from './index';
4
+
5
+ const { locale = 'en' } = Astro.props;
6
+ const content = await urbanGardenPlanner.i18n[locale as keyof typeof urbanGardenPlanner.i18n]?.();
7
+ ---
8
+
9
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'RHS Urban gardening',
6
+ url: 'https://www.rhs.org.uk/advice/urban-gardening',
7
+ },
8
+ {
9
+ name: 'US EPA Rain barrels',
10
+ url: 'https://www.epa.gov/soakuptherain/soak-rain-rain-barrels',
11
+ },
12
+ {
13
+ name: 'FAO Water harvesting and soil moisture management',
14
+ url: 'https://www.fao.org/4/y4690e/y4690e00.htm',
15
+ },
16
+ ];
@@ -0,0 +1,131 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { urbanGardenPlanner } from './index';
4
+ import { DEFAULT_INPUTS } from './logic';
5
+
6
+ const { ui: propUi, locale = 'en' } = Astro.props;
7
+ let ui = propUi;
8
+
9
+ if (!ui) {
10
+ const content = await urbanGardenPlanner.i18n[locale as keyof typeof urbanGardenPlanner.i18n]?.();
11
+ ui = content?.ui;
12
+ }
13
+
14
+ if (!ui) return null;
15
+
16
+ const config = JSON.stringify({ ui, defaults: DEFAULT_INPUTS });
17
+ ---
18
+
19
+ <urban-garden-planner class="ugrp-root">
20
+ <div class="ugrp-shell">
21
+ <section class="ugrp-input-panel" aria-labelledby="ugrp-input-heading">
22
+ <div class="ugrp-kicker">{ui.eyebrow}</div>
23
+ <h2 id="ugrp-input-heading" class="ugrp-heading">
24
+ <span class="ugrp-step">1</span>
25
+ {ui.headInputs}
26
+ </h2>
27
+
28
+ <div class="ugrp-fields">
29
+ <label class="ugrp-field" for="gardenArea">
30
+ <span class="ugrp-label"><Icon name="mdi:flower-outline" aria-hidden="true" />{ui.labelGardenArea}</span>
31
+ <span class="ugrp-input-line">
32
+ <input id="gardenArea" type="number" min="0" step="0.5" value={DEFAULT_INPUTS.gardenAreaM2} />
33
+ <span>{ui.unitSquareMeters}</span>
34
+ </span>
35
+ <span class="ugrp-help">{ui.helpGardenArea}</span>
36
+ </label>
37
+
38
+ <label class="ugrp-field" for="substrateDepth">
39
+ <span class="ugrp-label"><Icon name="mdi:layers-outline" aria-hidden="true" />{ui.labelDepth}</span>
40
+ <span class="ugrp-input-line">
41
+ <input id="substrateDepth" type="number" min="0" step="1" value={DEFAULT_INPUTS.substrateDepthCm} />
42
+ <span>{ui.unitCentimeters}</span>
43
+ </span>
44
+ <span class="ugrp-help">{ui.helpDepth}</span>
45
+ </label>
46
+
47
+ <label class="ugrp-field" for="collectionArea">
48
+ <span class="ugrp-label"><Icon name="mdi:home-roof" aria-hidden="true" />{ui.labelCollectionArea}</span>
49
+ <span class="ugrp-input-line">
50
+ <input id="collectionArea" type="number" min="0" step="0.5" value={DEFAULT_INPUTS.collectionAreaM2} />
51
+ <span>{ui.unitSquareMeters}</span>
52
+ </span>
53
+ <span class="ugrp-help">{ui.helpCollectionArea}</span>
54
+ </label>
55
+
56
+ <label class="ugrp-field" for="rainfall">
57
+ <span class="ugrp-label"><Icon name="mdi:weather-rainy" aria-hidden="true" />{ui.labelRainfall}</span>
58
+ <span class="ugrp-input-line">
59
+ <input id="rainfall" type="number" min="0" step="1" value={DEFAULT_INPUTS.rainfallMm} />
60
+ <span>{ui.unitMillimeters}</span>
61
+ </span>
62
+ <span class="ugrp-help">{ui.helpRainfall}</span>
63
+ </label>
64
+ </div>
65
+
66
+ <div class="ugrp-presets">
67
+ <div class="ugrp-subheading">{ui.presetsTitle}</div>
68
+ <div class="ugrp-preset-row">
69
+ <button type="button" class="ugrp-preset" data-preset="4,20,8,15">{ui.presetBalcony}</button>
70
+ <button type="button" class="ugrp-preset" data-preset="12,30,25,20">{ui.presetRaisedBed}</button>
71
+ <button type="button" class="ugrp-preset" data-preset="24,35,50,25">{ui.presetCommunity}</button>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="ugrp-note ugrp-note-assumption">
76
+ <Icon name="mdi:calculator-variant-outline" aria-hidden="true" />
77
+ <div><strong>{ui.assumptionTitle}</strong><span>{ui.assumptionText}</span></div>
78
+ </div>
79
+ </section>
80
+
81
+ <section class="ugrp-result-panel" aria-labelledby="ugrp-scene-heading">
82
+ <div class="ugrp-result-topline">
83
+ <div>
84
+ <div class="ugrp-kicker">{ui.headScene}</div>
85
+ <h2 id="ugrp-scene-heading" class="ugrp-heading"><span class="ugrp-step ugrp-step-light">2</span>{ui.sceneGardenBed}</h2>
86
+ </div>
87
+ <div id="planStatus" class="ugrp-status" aria-live="polite">{ui.statusBalanced}</div>
88
+ </div>
89
+
90
+ <div class="ugrp-landscape" aria-label={ui.headScene} role="img">
91
+ <div id="rainDots" class="ugrp-rain-dots" aria-hidden="true"></div>
92
+ <div class="ugrp-cloud"><Icon name="mdi:weather-cloudy" aria-hidden="true" /></div>
93
+ <div class="ugrp-catchment"><span>{ui.sceneCatchment}</span><i></i></div>
94
+ <div class="ugrp-pipe"><i></i><b></b></div>
95
+ <div class="ugrp-bed">
96
+ <div class="ugrp-bed-frame"></div>
97
+ <div id="soilFill" class="ugrp-soil-fill" role="progressbar" aria-valuemin="0" aria-valuemax="100"></div>
98
+ <div class="ugrp-leaves"><Icon name="mdi:sprout" aria-hidden="true" /><Icon name="mdi:sprout" aria-hidden="true" /><Icon name="mdi:sprout" aria-hidden="true" /></div>
99
+ <span>{ui.sceneSoil}</span>
100
+ </div>
101
+ <div class="ugrp-water-vessel"><div id="waterFill" role="progressbar" aria-valuemin="0" aria-valuemax="100"></div><Icon name="mdi:water-outline" aria-hidden="true" /><span>{ui.sceneWater}</span></div>
102
+ </div>
103
+
104
+ <p id="planStatusNote" class="ugrp-status-note">{ui.statusNote}</p>
105
+
106
+ <div class="ugrp-metrics" aria-live="polite">
107
+ <div class="ugrp-metric ugrp-metric-main"><span>{ui.labelWaterCaptured}</span><strong id="waterValue">0 L</strong></div>
108
+ <div class="ugrp-metric"><span>{ui.labelSoilNeeded}</span><strong id="soilValue">0 L</strong></div>
109
+ <div class="ugrp-metric"><span>{ui.labelGardenDepth}</span><strong id="depthValue">0 mm</strong></div>
110
+ <div class="ugrp-metric"><span>{ui.labelWateringArea}</span><strong id="areaValue">0 m²</strong></div>
111
+ </div>
112
+
113
+ <div class="ugrp-note ugrp-note-limit">
114
+ <Icon name="mdi:leaf-circle-outline" aria-hidden="true" />
115
+ <div><strong>{ui.limitationTitle}</strong><span>{ui.limitationText}</span></div>
116
+ </div>
117
+ </section>
118
+ </div>
119
+ </urban-garden-planner>
120
+
121
+ <script is:inline type="application/json" id="ugrp-config" set:html={config}></script>
122
+
123
+ <script>
124
+ import { mountUrbanGardenPlanner } from './controller';
125
+ const root = document.querySelector<HTMLElement>('urban-garden-planner');
126
+ const configNode = document.getElementById('ugrp-config');
127
+ if (root && configNode) {
128
+ const config = JSON.parse(configNode.textContent || '{}');
129
+ mountUrbanGardenPlanner(root, config);
130
+ }
131
+ </script>
@@ -0,0 +1,88 @@
1
+ import { evaluateGardenPlan } from './evaluator';
2
+ import { calculateGardenPlan, DEFAULT_INPUTS, type GardenPlanInput } from './logic';
3
+ import { findElements, renderGardenPlan, type GardenPlannerElements } from './dom-views';
4
+ import { loadGardenPlan, saveGardenPlan } from './storage';
5
+ import type { UrbanGardenPlannerUI } from './ui';
6
+
7
+ export interface UrbanGardenPlannerConfig {
8
+ ui: UrbanGardenPlannerUI;
9
+ defaults: GardenPlanInput;
10
+ }
11
+
12
+ const inputKeys: (keyof GardenPlanInput)[] = [
13
+ 'gardenAreaM2',
14
+ 'substrateDepthCm',
15
+ 'collectionAreaM2',
16
+ 'rainfallMm',
17
+ ];
18
+
19
+ class UrbanGardenPlannerController {
20
+ private readonly elements: GardenPlannerElements;
21
+ private readonly config: UrbanGardenPlannerConfig;
22
+
23
+ constructor(private readonly root: HTMLElement, config: UrbanGardenPlannerConfig) {
24
+ this.elements = findElements(root);
25
+ this.config = config;
26
+ }
27
+
28
+ mount(): void {
29
+ this.restoreInputs();
30
+ this.bindInputs();
31
+ this.bindPresets();
32
+ this.calculate();
33
+ }
34
+
35
+ private restoreInputs(): void {
36
+ const saved = loadGardenPlan();
37
+ const values = { ...DEFAULT_INPUTS, ...this.config.defaults, ...saved };
38
+ inputKeys.forEach((key) => {
39
+ const element = this.elements.inputs[key];
40
+ if (element) element.value = String(values[key]);
41
+ });
42
+ }
43
+
44
+ private bindInputs(): void {
45
+ inputKeys.forEach((key) => {
46
+ this.elements.inputs[key]?.addEventListener('input', () => this.calculate());
47
+ });
48
+ }
49
+
50
+ private bindPresets(): void {
51
+ this.root.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((button) => {
52
+ button.addEventListener('click', () => {
53
+ const values = button.dataset.preset?.split(',').map(Number) ?? [];
54
+ inputKeys.forEach((key, index) => {
55
+ const element = this.elements.inputs[key];
56
+ if (element && Number.isFinite(values[index])) element.value = String(values[index]);
57
+ });
58
+ this.calculate();
59
+ });
60
+ });
61
+ }
62
+
63
+ private readInputs(): GardenPlanInput {
64
+ return {
65
+ gardenAreaM2: Number(this.elements.inputs.gardenAreaM2?.value),
66
+ substrateDepthCm: Number(this.elements.inputs.substrateDepthCm?.value),
67
+ collectionAreaM2: Number(this.elements.inputs.collectionAreaM2?.value),
68
+ rainfallMm: Number(this.elements.inputs.rainfallMm?.value),
69
+ };
70
+ }
71
+
72
+ private calculate(): void {
73
+ const input = this.readInputs();
74
+ const result = calculateGardenPlan(input);
75
+ const assessment = evaluateGardenPlan(input, result);
76
+ saveGardenPlan(input);
77
+ renderGardenPlan(this.elements, result, assessment, this.config.ui);
78
+ }
79
+ }
80
+
81
+ export function mountUrbanGardenPlanner(
82
+ root: HTMLElement,
83
+ config: UrbanGardenPlannerConfig,
84
+ ): void {
85
+ if (root.dataset.mounted === 'true') return;
86
+ root.dataset.mounted = 'true';
87
+ new UrbanGardenPlannerController(root, config).mount();
88
+ }
@@ -0,0 +1,84 @@
1
+ import type { GardenPlanAssessment } from './evaluator';
2
+ import type { GardenPlanResult } from './logic';
3
+ import type { UrbanGardenPlannerUI } from './ui';
4
+
5
+ export interface GardenPlannerElements {
6
+ root: HTMLElement;
7
+ inputs: Record<keyof GardenPlanResult, HTMLInputElement | null>;
8
+ soilValue: HTMLElement | null;
9
+ waterValue: HTMLElement | null;
10
+ depthValue: HTMLElement | null;
11
+ areaValue: HTMLElement | null;
12
+ status: HTMLElement | null;
13
+ statusNote: HTMLElement | null;
14
+ soilFill: HTMLElement | null;
15
+ waterFill: HTMLElement | null;
16
+ rainDots: HTMLElement | null;
17
+ }
18
+
19
+ export function findElements(root: HTMLElement): GardenPlannerElements {
20
+ const input = (id: string) => root.querySelector<HTMLInputElement>(id);
21
+ const output = (id: string) => root.querySelector<HTMLElement>(id);
22
+
23
+ return {
24
+ root,
25
+ inputs: {
26
+ gardenAreaM2: input('#gardenArea'),
27
+ substrateDepthCm: input('#substrateDepth'),
28
+ collectionAreaM2: input('#collectionArea'),
29
+ rainfallMm: input('#rainfall'),
30
+ soilVolumeLiters: null,
31
+ grossRainwaterLiters: null,
32
+ harvestedWaterLiters: null,
33
+ gardenWaterDepthMm: null,
34
+ wateringAreaAt10Mm: null,
35
+ },
36
+ soilValue: output('#soilValue'),
37
+ waterValue: output('#waterValue'),
38
+ depthValue: output('#depthValue'),
39
+ areaValue: output('#areaValue'),
40
+ status: output('#planStatus'),
41
+ statusNote: output('#planStatusNote'),
42
+ soilFill: output('#soilFill'),
43
+ waterFill: output('#waterFill'),
44
+ rainDots: output('#rainDots'),
45
+ };
46
+ }
47
+
48
+ export function formatNumber(value: number, maximumFractionDigits = 0): string {
49
+ return new Intl.NumberFormat('en', { maximumFractionDigits }).format(value);
50
+ }
51
+
52
+ function setText(element: HTMLElement | null, text: string): void {
53
+ if (element) element.textContent = text;
54
+ }
55
+
56
+ function statusLabel(ui: UrbanGardenPlannerUI, assessment: GardenPlanAssessment): string {
57
+ const labels = {
58
+ balanced: ui.statusBalanced,
59
+ lightCapture: ui.statusLightCapture,
60
+ deepBed: ui.statusDeepBed,
61
+ noRain: ui.statusNoRain,
62
+ };
63
+ return labels[assessment.status];
64
+ }
65
+
66
+ export function renderGardenPlan(
67
+ elements: GardenPlannerElements,
68
+ result: GardenPlanResult,
69
+ assessment: GardenPlanAssessment,
70
+ ui: UrbanGardenPlannerUI,
71
+ ): void {
72
+ setText(elements.soilValue, `${formatNumber(result.soilVolumeLiters)} L`);
73
+ setText(elements.waterValue, `${formatNumber(result.harvestedWaterLiters, 1)} L`);
74
+ setText(elements.depthValue, `${formatNumber(result.gardenWaterDepthMm, 1)} mm`);
75
+ setText(elements.areaValue, `${formatNumber(result.wateringAreaAt10Mm, 1)} m²`);
76
+ setText(elements.status, statusLabel(ui, assessment));
77
+ setText(elements.statusNote, ui.statusNote);
78
+ elements.root.dataset.status = assessment.status;
79
+ elements.root.style.setProperty('--ugrp-water-progress', `${assessment.progress}%`);
80
+ elements.root.style.setProperty('--ugrp-soil-progress', `${Math.min(92, Math.max(18, result.substrateDepthCm * 1.7))}%`);
81
+ elements.root.style.setProperty('--ugrp-rain-progress', `${Math.min(100, Math.max(8, result.rainfallMm * 2.2))}%`);
82
+ if (elements.soilFill) elements.soilFill.setAttribute('aria-valuenow', String(result.substrateDepthCm));
83
+ if (elements.waterFill) elements.waterFill.setAttribute('aria-valuenow', String(result.harvestedWaterLiters));
84
+ }
@@ -0,0 +1,29 @@
1
+ import type { NatureToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { UrbanGardenPlannerUI } from './ui';
3
+
4
+ export type UrbanGardenPlannerLocaleContent = ToolLocaleContent<UrbanGardenPlannerUI>;
5
+
6
+ export const urbanGardenPlanner: NatureToolEntry<UrbanGardenPlannerUI> = {
7
+ id: 'urban-garden-rainwater-soil-planner',
8
+ icons: {
9
+ bg: 'mdi:water-outline',
10
+ fg: 'mdi:sprout',
11
+ },
12
+ i18n: {
13
+ en: async () => (await import('./i18n/en')).content,
14
+ de: async () => (await import('./i18n/de')).content,
15
+ es: async () => (await import('./i18n/es')).content,
16
+ fr: async () => (await import('./i18n/fr')).content,
17
+ id: async () => (await import('./i18n/id')).content,
18
+ it: async () => (await import('./i18n/it')).content,
19
+ ja: async () => (await import('./i18n/ja')).content,
20
+ ko: async () => (await import('./i18n/ko')).content,
21
+ nl: async () => (await import('./i18n/nl')).content,
22
+ pl: async () => (await import('./i18n/pl')).content,
23
+ pt: async () => (await import('./i18n/pt')).content,
24
+ ru: async () => (await import('./i18n/ru')).content,
25
+ sv: async () => (await import('./i18n/sv')).content,
26
+ tr: async () => (await import('./i18n/tr')).content,
27
+ zh: async () => (await import('./i18n/zh')).content,
28
+ },
29
+ };
@@ -0,0 +1,27 @@
1
+ import type { GardenPlanInput, GardenPlanResult } from './logic';
2
+
3
+ export type GardenPlanStatus = 'balanced' | 'lightCapture' | 'deepBed' | 'noRain';
4
+
5
+ export interface GardenPlanAssessment {
6
+ status: GardenPlanStatus;
7
+ progress: number;
8
+ }
9
+
10
+ export function evaluateGardenPlan(
11
+ input: GardenPlanInput,
12
+ result: GardenPlanResult,
13
+ ): GardenPlanAssessment {
14
+ if (result.rainfallMm === 0 || result.collectionAreaM2 === 0) {
15
+ return { status: 'noRain', progress: 8 };
16
+ }
17
+
18
+ if (result.gardenWaterDepthMm < 5) {
19
+ return { status: 'lightCapture', progress: Math.max(12, result.gardenWaterDepthMm * 8) };
20
+ }
21
+
22
+ if (input.substrateDepthCm > 45) {
23
+ return { status: 'deepBed', progress: 86 };
24
+ }
25
+
26
+ return { status: 'balanced', progress: Math.min(92, Math.max(18, result.gardenWaterDepthMm * 3)) };
27
+ }
@@ -0,0 +1,50 @@
1
+ import type { FAQPage, HowToThing, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { UrbanGardenPlannerLocaleContent } from '../entry';
4
+
5
+ const slug = 'gartenboden-und-regenwasser-planer';
6
+ const title = 'Planer für Gartenboden und Regenwasser';
7
+ const description = 'Berechne das Substratvolumen für Hochbeete und schätze die Regenwassermenge für kleine Stadtgärten.';
8
+
9
+ const faqData = [
10
+ { question: 'Wie viel Erde brauche ich für ein Hochbeet?', answer: 'Gib die Anbaufläche und die Substrattiefe ein. Der Planer multipliziert beide Werte und rechnet das Ergebnis in Liter um, damit du Erde oder Kompost besser bestellen kannst.' },
11
+ { question: 'Wie berechne ich Regenwasser für einen Garten?', answer: 'Gib die Sammelfläche und die Niederschlagshöhe ein. Der Planer wendet danach einen Abflussfaktor von 85 Prozent und einen Sammelfaktor von 90 Prozent als vorsichtige Planungsschätzung an.' },
12
+ { question: 'Funktioniert der Planer für einen Balkon?', answer: 'Ja. Verwende die Fläche des Pflanzbereichs, die geplante Substrattiefe und die Dach- oder Vordachfläche, die deinen Behälter speist.' },
13
+ { question: 'Ist gesammeltes Regenwasser für essbare Pflanzen sicher?', answer: 'Nicht automatisch. Prüfe Sammelfläche und Behälter, sorge für geeignete Filter und Entwässerung und beachte die örtlichen Hinweise.' },
14
+ ];
15
+
16
+ const howToData = [
17
+ { name: 'Anbaufläche messen', text: 'Gib die Fläche des Beets, Balkons oder Pflanzgefäßes ein, die das Substrat aufnehmen soll.' },
18
+ { name: 'Substrattiefe festlegen', text: 'Trage die geplante Tiefe in Zentimetern ein, einschließlich möglicher Setzung.' },
19
+ { name: 'Sammelfläche eintragen', text: 'Gib die Dach-, Vordach- oder andere Fläche ein, die Wasser in den Behälter leitet.' },
20
+ { name: 'Regenereignis testen', text: 'Trage eine Niederschlagshöhe in Millimetern ein und vergleiche das Wasser mit der Gartengröße.' },
21
+ ];
22
+
23
+ const faqSchema: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqData.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: WithContext<HowToThing> = { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howToData.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) };
25
+ const appSchema: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'UtilityApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }, inLanguage: 'de' };
26
+
27
+ export const content: UrbanGardenPlannerLocaleContent = {
28
+ slug,
29
+ title,
30
+ description,
31
+ ui: {
32
+ eyebrow: 'Ein kleiner Garten, in lebendigen Schichten gemessen', headInputs: 'Gartenszene festlegen', headScene: 'Die Landschaft lesen', labelGardenArea: 'Anbaufläche', labelDepth: 'Substrattiefe', labelCollectionArea: 'Sammelfläche', labelRainfall: 'Regenereignis', unitSquareMeters: 'm²', unitCentimeters: 'cm', unitMillimeters: 'mm', unitLiters: 'L', helpGardenArea: 'Fläche von Beet, Balkon oder Pflanzgefäß.', helpDepth: 'Die Bodenschicht, die du auffüllen möchtest.', helpCollectionArea: 'Fläche, die Wasser in deinen Behälter leitet.', helpRainfall: 'Teste einen Schauer oder ein örtliches Planungsereignis.', presetsTitle: 'Mit einer Form beginnen', presetBalcony: 'Balkonbeet', presetRaisedBed: 'Hochbeet', presetCommunity: 'Gemeinschaftsgarten', labelSoilNeeded: 'Benötigtes Substrat', labelWaterCaptured: 'Gesammeltes Wasser', labelGardenDepth: 'Auf dieser Gartenfläche', labelWateringArea: 'Bei 10 mm Wasser', sceneCatchment: 'Sammelfläche', sceneGardenBed: 'Anbaufläche', sceneSoil: 'Bodenschicht', sceneWater: 'Gesammeltes Wasser', statusBalanced: 'Eine brauchbare Planungskombination', statusLightCapture: 'Ein kleines Regenereignis', statusDeepBed: 'Ein tiefes Bodenprofil', statusNoRain: 'Warte auf ein Regenereignis', statusNote: 'Nutze diese Momentaufnahme zum Vergleichen von Materialien und Szenarien. Sie ist keine Bewässerungsempfehlung.', assumptionTitle: 'Planungsannahmen', assumptionText: 'Die Schätzung rechnet mit 85 Prozent Abfluss und 90 Prozent nach Verlusten bei der Sammlung. Ein Liter auf einem Quadratmeter entspricht einem Millimeter Wasser.', limitationTitle: 'Garten sicher nutzen', limitationText: 'Sorge für Entwässerung, vermeide belastete Sammelflächen und prüfe örtliche Regeln, bevor du Regenwasser für essbare Pflanzen verwendest.',
33
+ },
34
+ seo: [
35
+ { type: 'title', text: 'Gartenboden und Regenwasser für kleine Flächen berechnen', level: 2 },
36
+ { type: 'paragraph', html: 'Ein Hochbeet, ein Balkon und ein Gemeinschaftsgarten brauchen dieselbe praktische Klarheit: Wie viel Substrat wird benötigt und wie viel Regenwasser kann eine verfügbare Fläche ungefähr sammeln? Dieser Planer stellt beide Schätzungen nebeneinander.' },
37
+ { type: 'title', text: 'Formel für das Substratvolumen', level: 3 },
38
+ { type: 'paragraph', html: 'Die Rechnung lautet <code>Fläche in m² × Tiefe in cm × 10 = Liter Substrat</code>. Ein Beet mit 12 m² Fläche und 30 cm Tiefe benötigt damit 3.600 Liter, bevor Setzung, Drainageschichten oder die konkrete Bepflanzung berücksichtigt werden.' },
39
+ { type: 'list', items: ['<strong>Anbaufläche:</strong> Grundfläche von Beet, Balkon oder Pflanzgefäß.', '<strong>Substrattiefe:</strong> geplante Höhe der Bodenschicht.', '<strong>Sammelfläche:</strong> Dach, Vordach oder andere Zuflussfläche.', '<strong>Regenereignis:</strong> Niederschlagshöhe in Millimetern.'] },
40
+ { type: 'title', text: 'Regenwasser für den Garten schätzen', level: 3 },
41
+ { type: 'paragraph', html: 'Die Wassermenge beginnt mit <code>Sammelfläche × Niederschlag</code>. Danach werden 85 Prozent für Abflussverluste und 90 Prozent für Verluste bei der Sammlung angesetzt. So entsteht eine nachvollziehbare Schätzung statt eines Versprechens über die tatsächlich verfügbare Wassermenge.' },
42
+ { type: 'title', text: 'Szenarien offline vergleichen', level: 3 },
43
+ { type: 'paragraph', html: 'Teste ein Balkonbeet, ein Hochbeet und eine größere Gemeinschaftsfläche mit den Presets und ersetze sie danach durch deine Messwerte. Der Planer benötigt keine Adresse, Karte oder Wetterverbindung und bleibt im Browser.' },
44
+ { type: 'tip', title: 'Wichtige Grenzen', html: 'Entwässerung, Wasserqualität, Verdunstung, Pflanzenwahl und örtliche Vorschriften brauchen weiterhin eine menschliche Entscheidung. Belastete Dächer oder Behälter sind für essbare Pflanzen nicht automatisch geeignet.' },
45
+ ],
46
+ faq: faqData,
47
+ howTo: howToData,
48
+ bibliography,
49
+ schemas: [faqSchema, howToSchema, appSchema],
50
+ };