Skip to content

feat(web): draw your PV arrays on the map - #826

Draft
HuggeK wants to merge 2 commits into
srcfl:masterfrom
HuggeK:pv-array-draw
Draft

feat(web): draw your PV arrays on the map#826
HuggeK wants to merge 2 commits into
srcfl:masterfrom
HuggeK:pv-array-draw

Conversation

@HuggeK

@HuggeK HuggeK commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Typing tilt, azimuth and kWp for a roof you can see out of the window is the worst part of setting FTW up — and the numbers people type are guesses. The Weather tab's existing Leaflet map now takes a rectangle drawn over the panels and turns it into a weather.pv_arrays entry.

This is phase 5-manual from the roadmap in discussion #717, the one item there with no gating: it needs no Geotorget account, no LiDAR and no roof module.

What drawing can and cannot tell you

A drawn rectangle answers two of the three questions an array asks, and is honest about the third.

Where it comes from
Area → kWp the shape, projected to metres and measured
Azimuth the way the shape is turned
Tilt typed — an overhead outline cannot contain it

The rectangle is drawn at an angle rather than square to north, because the angle is where the answer lives: the long edge follows the ridge, and the face is perpendicular to it.

Why the azimuth is offered rather than asserted

Two directions are perpendicular to a ridge, and a flat outline genuinely does not say which one the panels face. The equatorward candidate is filled in as a default and both candidates are shown, with a one-click flip. Calling that a measurement would be inventing information the drawing does not contain.

The detail that would otherwise under-size every array

What you trace on a map is the horizontal projection of a sloped rectangle, not the rectangle. A sloped area A casts a shadow of A·cos(tilt), so recovering the real panel area divides that back out — a 35° roof carries about 22 % more panel than its outline suggests.

That is also why tilt is typed before drawing rather than after: it is an input to the area, not a label on it. Capacity then uses the same 0.70 packing factor and 200 W/m² module density as the Lantmäteriet roof model, so a drawn array and a derived one are directly comparable.

Testing

The geometry is a pure ES module (web/components/pv-array-geometry.js) with 21 tests.

Why the fixtures are not built with the module's own projection

Round-tripping a shape through the code under test would make it agree with itself and prove nothing. The fixtures are built from the standard ellipsoidal metres-per-degree series, deliberately a different model from the module's spherical tangent plane. The two disagree by ~0.5 % in area at Stockholm's latitude — the known bias of a spherical Earth against WGS84, and far below the precision anyone draws a roof with — so the tolerances document that gap instead of hiding it.

Covered: area against a known 10 × 6 m rectangle, winding-order and closing-vertex independence, ridge bearing as a line rather than an arrow, both azimuth candidates, hemisphere-dependent defaults, rotation, the flip, the plan→slope conversion, and that the object written back carries only the four fields weather.pv_arrays defines.

Dependencies

Terra Draw 1.32.2 and its Leaflet adapter 1.3.0, both MIT, lazy-loaded from CDN only when the tool is first used — the same pattern as today's Leaflet loader, and no new install on the host.

Why UMD, and one load-bearing alias

Both packages ship UMD builds, so they load behind real SRI hashes. That is strictly better than the ES-module route, where SRI does not propagate into a module's own imports and version pinning is all you get.

One trap, found by reading the adapter bundle rather than assuming: its UMD factory captures window.leaflet as it evaluates and uses it for its own vertex markers (a.default.divIcon, a.default.marker), while Leaflet only ever defines window.L. Without the alias the adapter loads fine and then throws on the first vertex. The alias is load-bearing, not tidiness.

Verification

  • node --test web/**/*.test.mjs316 tests, 315 pass; the one failure is theme-tokens.test.mjs, which fails identically on a clean master on Windows (path separators).
  • Progressive enhancement: if either bundle fails to load, the numeric editor is untouched and the status line says why.

Browser-verified since: the full interaction has now been driven in headless Edge over CDP against a running FTW — both bundles loaded from CDN with their SRI hashes, an angled rectangle drawn with three clicks over the OSM map at zoom 18, the finish event fired, and the status line reported “Roof south added: 618.9 m² outline is 755.5 m² of roof at 35°, about 105.77 kWp. Facing 168° — the outline fits 168° and 348° equally well.” (755.5/618.9 = 1/cos 35°.) The array appeared in the numeric editor and the 3D preview, and the flip button turned it to 348° with the form following. The window.leaflet alias was exercised live — vertex markers drew without a throw.

🤖 Generated with Claude Code

Seen running

Three clicks over the live OSM map — corner, along the ridge, out to depth — in headless Edge, with both UMD bundles loading from CDN behind their SRI hashes:

The angled rectangle drawn over the map

The rectangle became a form entry — 105.77 kWp, 35°, 168° — with the 3D preview rendering the plane. 618.9 m² of outline is 755.5 m² of roof: exactly 1/cos 35°.

The drawn array in the numeric editor with 3D preview

One click on Flip 180° turned it to 348° and the form followed:

The flipped array facing 348°

Runbook: how this was demoed (reproducible)
  1. go build -o ftw-draw.exe ./cmd/ftw from this branch.
  2. A minimal config: Stockholm (59.3293, 18.0686), provider: open_meteo, no drivers needed for the Weather tab.
  3. Launch, open Settings → Weather, zoom the Leaflet map to 18 via window._weatherMap.setView(...), press ✎ Draw on the map.
  4. Driven with headless Edge over the DevTools protocol — the three clicks are Input.dispatchMouseEvent at viewport coordinates; screenshots are unretouched viewport captures. Nothing installed.

claude and others added 2 commits August 5, 2026 12:59
Typing tilt, azimuth and kWp for a roof you can see out of the window is
the worst part of setting FTW up, and the numbers people type are guesses.
The Weather tab's existing Leaflet map now takes a rectangle drawn over
the panels and turns it into a weather.pv_arrays entry.

The rectangle is angled rather than square to north, because the angle is
where the answer lives: its long edge follows the ridge, and the face is
perpendicular to that. Two directions are perpendicular to a ridge and an
overhead outline genuinely does not say which, so the equatorward one is
offered as a default with a one-click flip, never as a measurement.

Tilt cannot be seen from above at all, so it is typed once before drawing
-- and it is also what converts the outline into panel area. What you
trace on a map is the horizontal projection of a sloped rectangle, so a
35 deg roof carries about 22% more panel than its outline suggests;
without that division every drawn array would be quietly under-sized.
Capacity uses the same 0.70 packing factor and 200 W/m2 module density as
the Lantmateriet roof model, so a drawn array and a derived one compare.

The geometry is a pure ES module, tested against fixtures built from the
standard ellipsoidal metres-per-degree series rather than from its own
spherical projection -- a shape round-tripped through the code under test
would have agreed with itself and proved nothing.

Terra Draw and its Leaflet adapter (both MIT) ship UMD builds, so they
lazy-load behind real SRI hashes instead of the bare version pinning an
ES module would have forced. The adapter captures window.leaflet as it
evaluates while Leaflet only ever defines window.L, so that alias is
load-bearing rather than tidiness. If either bundle fails to load, the
numeric editor is untouched and the page says so.

Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com>
@HuggeK

HuggeK commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

The MapLibre port this PR will need is worked out and verified

The roofmodel stack (#734#736) replaces the Weather tab's Leaflet picker with MapLibre GL, so whichever of us lands second, this PR's drawing glue has to swap adapters. That port now exists and has been driven in a browser on a combined build: branch demo-combined (merge commit).

The swap is smaller than the original integration, because the MapLibre adapter is better-behaved than the Leaflet one:

Leaflet adapter (this PR) MapLibre adapter (the port)
Package terra-draw-leaflet-adapter 1.3.0 terra-draw-maplibre-gl-adapter 1.4.1 (MIT)
Global it needs at load terraDraw and window.leaflet (the alias) terraDraw only
Constructor { lib: window.L, map } { map }
SRI pinned sha384-A56++Zl2ljSDy1B+lcDdkRT3BVbzDkMolLzWffRCxiU5rGUWcUYDMvg/Dxr0JR+N

Everything above the adapter — the geometry module, its 21 tests, the finish handler, the flip — merges unchanged: Terra Draw's API is adapter-agnostic at that line.

Seen running on the combined buildRead roof from LiDAR filled the first array, then three clicks on the same MapLibre map drew the second:

Angled rectangle drawn on the MapLibre map

Derived and drawn arrays together in the editor

The editor ends up holding both: the LiDAR-derived Roof south 28.49 kWp @ 180° and the drawn Roof south 19.98 kWp @ 165° (116.9 m² outline → 142.7 m² at 35°, still exactly 1/cos 35°), both rendered in the 3D preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants