Add printable yard map and sprinkler zones with estimated coverage
Check and deploy / deploy (push) Successful in 25s
Check and deploy / deploy (push) Successful in 25s
This commit is contained in:
1 parent
c8c26c1706
commit
6fd308aca8
14 files changed
+608
-36
No files matched your search
@@ -124,3 +124,17 @@ of 24.
|
|||||||
|
|
||||||
- Decide whether parent-child feature relationships should further affect the visibility hierarchy.
|
- Decide whether parent-child feature relationships should further affect the visibility hierarchy.
|
||||||
- If more data import is needed from GIS sources, prefer one-time extraction into JS-authored modules rather than making the app depend on runtime GIS tooling.
|
- If more data import is needed from GIS sources, prefer one-time extraction into JS-authored modules rather than making the app depend on runtime GIS tooling.
|
||||||
|
|
||||||
|
### Sprinkler map
|
||||||
|
|
||||||
|
`src/data/irrigation.js` contains the heads transcribed from the annotated printout,
|
||||||
|
organized into visibility groups for zones 1–4. Positions, types, and coverage are
|
||||||
|
initial estimates. Near the house, the head marked 2 is a zone 2 mist head;
|
||||||
|
the adjacent head marked 4 is a zone 4 rotator. Each head has a stable zone/head ID.
|
||||||
|
|
||||||
|
Head coordinates are authored in feet east and south of `lotCorner`. `reachFeet`
|
||||||
|
is the spray radius. `startBearingDegrees` and `arcDegrees` describe a clockwise
|
||||||
|
sector; the authoring helper converts lot-relative bearings to true bearings.
|
||||||
|
Mist heads use a dotted fan symbol and dotted coverage; rotators use a sweep-arrow
|
||||||
|
symbol and dashed coverage. Coverage and zone labels appear on screen and in print.
|
||||||
|
Select a head to inspect its estimated dimensions and notes.
|
||||||
@@ -18,6 +18,12 @@
|
|||||||
a variety of trees, shrubs, and flowers. There are many varieties of
|
a variety of trees, shrubs, and flowers. There are many varieties of
|
||||||
daylilies.
|
daylilies.
|
||||||
</p>
|
</p>
|
||||||
|
<button id="print-map" type="button">Print map</button>
|
||||||
|
<p class="print-hint">Prints selected features at the selected date, aligned with the lot.</p>
|
||||||
|
<div class="sprinkler-legend" aria-label="Sprinkler symbols">
|
||||||
|
<span><img src="./src/assets/icons/sprinkler-mist.svg" alt="" /> Mist</span>
|
||||||
|
<span><img src="./src/assets/icons/sprinkler-rotator.svg" alt="" /> Rotator</span>
|
||||||
|
</div>
|
||||||
<section class="visibility-panel" aria-labelledby="visibility-title">
|
<section class="visibility-panel" aria-labelledby="visibility-title">
|
||||||
<h2 id="visibility-title">Map features</h2>
|
<h2 id="visibility-title">Map features</h2>
|
||||||
<div id="visibility-tree"></div>
|
<div id="visibility-tree"></div>
|
||||||
@@ -30,6 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="print-sheet" aria-label="Printable yard map"></div>
|
||||||
|
|
||||||
<script src="./src/error-overlay.js"></script>
|
<script src="./src/error-overlay.js"></script>
|
||||||
<script src="./vendor/pmtiles.js"></script>
|
<script src="./vendor/pmtiles.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<title>Mist sprinkler — continuous spray</title>
|
||||||
|
<circle cx="16" cy="16" r="15" fill="white" fill-opacity=".94" stroke="#087f8c"/>
|
||||||
|
<path d="M8 23A15 15 0 0 1 24 7L16 23Z" fill="#c5eef0"/>
|
||||||
|
<g fill="none" stroke="#087f8c" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M9 18l-1-2m6 0v-3m4 3 2-2m0 7 3-1"/>
|
||||||
|
<path d="M8 10h0m6-2h0m10 4h0m2 7h0" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="13" cy="23" r="3" fill="#087f8c" stroke="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<title>Rotator sprinkler — sweeping stream</title>
|
||||||
|
<circle cx="16" cy="16" r="15" fill="white" fill-opacity=".94" stroke="#3457b2"/>
|
||||||
|
<path d="M8 18a10 10 0 0 1 17-6" fill="none" stroke="#3457b2" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="m24 6 2 7-7-1" fill="none" stroke="#3457b2" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<path d="m13 23 7-13" stroke="#3457b2" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="13" cy="23" r="3" fill="#3457b2" stroke="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 560 B |
+69
-3
@@ -1,8 +1,74 @@
|
|||||||
import { east, ft, offset, south } from "../lib/geometry.js";
|
import { east, ft, offset, south } from "../lib/geometry.js";
|
||||||
import { Sprinkler } from "../lib/yard-features.js";
|
import { defineGroup, Sprinkler } from "../lib/yard-features.js";
|
||||||
import { lotCorner } from "./anchors.js";
|
import { lotCorner } from "./anchors.js";
|
||||||
|
|
||||||
|
export const irrigationGroup = defineGroup("group:irrigation", "Sprinklers", {
|
||||||
|
categoryId: "infrastructure",
|
||||||
|
});
|
||||||
|
const zones = [1, 2, 3, 4].map((zone) => defineGroup(
|
||||||
|
`group:irrigation-zone-${zone}`, `Zone ${zone}`, {
|
||||||
|
categoryId: "infrastructure", parentGroupId: irrigationGroup.id,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
export const irrigationGroups = [irrigationGroup, ...zones];
|
||||||
|
|
||||||
|
// Transcribed from the annotated October 14, 2026 printout using its 10 ft grid.
|
||||||
|
// Locations are approximate. All reaches and arcs are initial visual estimates.
|
||||||
|
// Coordinates: feet east/south of lotCorner; negative east is the boulevard.
|
||||||
|
// Bearings below are clockwise from LOT north; coverage exports true bearings.
|
||||||
|
function head(zone, number, name, eastFeet, southFeet, headType, reachFeet,
|
||||||
|
startBearingDegrees, arcDegrees, note) {
|
||||||
|
return new Sprinkler(`Zone ${zone} · ${number} · ${name}`, {
|
||||||
|
zone: String(zone), headType, reachFeet,
|
||||||
|
startBearingDegrees: startBearingDegrees + lotCorner.assumedNorthClockwiseDegrees,
|
||||||
|
arcDegrees, coverageEstimated: true,
|
||||||
|
note: note ?? "Position read from annotated printout; reach and arc estimated.",
|
||||||
|
})
|
||||||
|
.withId(`sprinkler:zone-${zone}-${number}`)
|
||||||
|
.inGroup(zones[zone - 1])
|
||||||
|
.add(offset(lotCorner), east(ft(eastFeet)), south(ft(southFeet)));
|
||||||
|
}
|
||||||
|
|
||||||
export const irrigationFeatures = [
|
export const irrigationFeatures = [
|
||||||
new Sprinkler("Front bed sprinkler").add(offset(lotCorner), south(ft(15)), east(ft(10))),
|
// Zone 1: boulevard north of the entry walk, and north front lawn.
|
||||||
new Sprinkler("Garage side sprinkler").add(offset(lotCorner), south(ft(92)), east(ft(40))),
|
head(1, 1, "Boulevard northwest", -18.5, 0.5, "mist", 10, 90, 90),
|
||||||
|
head(1, 2, "Boulevard northeast", -5, 0.5, "mist", 10, 180, 90),
|
||||||
|
head(1, 3, "Boulevard southwest", -18.5, 11.5, "mist", 10, 0, 90),
|
||||||
|
head(1, 4, "Boulevard southeast", -5, 11.5, "mist", 10, 270, 90),
|
||||||
|
head(1, 5, "Front lawn northwest", 5.5, 10, "mist", 10, 0, 90),
|
||||||
|
head(1, 6, "Front walk bend", 19, 10, "mist", 10, 270, 90),
|
||||||
|
head(1, 7, "Front lawn west", 5.5, 17, "mist", 15, 90, 90),
|
||||||
|
head(1, 8, "Front porch edge", 22, 17, "mist", 16, 180, 90),
|
||||||
|
head(1, 9, "Front lawn south", 17.5, 32.5, "mist", 16, 270, 90),
|
||||||
|
|
||||||
|
// Zone 2: paired heads down the longer southern boulevard strip.
|
||||||
|
head(2, 1, "Boulevard northwest", -18.5, 15.5, "mist", 12, 90, 90),
|
||||||
|
head(2, 2, "Boulevard northeast", -5, 15.5, "mist", 12, 180, 90),
|
||||||
|
head(2, 3, "Boulevard west upper", -18.5, 28, "mist", 12, 0, 180),
|
||||||
|
head(2, 4, "Boulevard east upper", -5, 28, "mist", 12, 180, 180),
|
||||||
|
head(2, 5, "Boulevard west lower", -18.5, 39, "mist", 12, 0, 180),
|
||||||
|
head(2, 6, "Boulevard east lower", -5, 39, "mist", 12, 180, 180),
|
||||||
|
head(2, 7, "Boulevard southwest", -18.5, 50.5, "mist", 12, 0, 90),
|
||||||
|
head(2, 8, "Boulevard southeast", -5, 50.5, "mist", 12, 270, 90),
|
||||||
|
|
||||||
|
// Zone 3: south front lawn and the narrow lawn along the house.
|
||||||
|
head(3, 1, "Front west upper", 5, 28, "rotator", 12, 90, 90),
|
||||||
|
head(3, 2, "Front west lower", 5.5, 43, "rotator", 12, 0, 90),
|
||||||
|
head(3, 3, "Porch southwest", 28, 37.5, "rotator", 10, 180, 90),
|
||||||
|
head(3, 4, "South fence by front bed", 26, 49.5, "rotator", 15, 270, 90),
|
||||||
|
head(3, 5, "South bed northeast", 42, 38, "rotator", 12, 90, 90),
|
||||||
|
head(3, 6, "South fence by bed", 44, 49.5, "rotator", 12, 0, 90),
|
||||||
|
head(3, 7, "House south wall", 57, 31, "rotator", 12, 90, 180),
|
||||||
|
|
||||||
|
// Zone 4: rear lawn. The adjacent head marked 2 belongs to zone 2.
|
||||||
|
head(4, 1, "Rear lawn northwest", 80, 6, "rotator", 15, 90, 90),
|
||||||
|
head(4, 2, "Rear lawn northeast", 106, 6, "rotator", 15, 180, 90),
|
||||||
|
head(4, 3, "Rear lawn southwest", 80, 23.5, "rotator", 15, 0, 90),
|
||||||
|
head(4, 4, "Rear lawn southeast", 105, 25, "rotator", 15, 270, 90),
|
||||||
|
head(2, 9, "House southeast mist", 82, 34.5, "mist", 12, 90, 90,
|
||||||
|
"Confirmed zone 2 mist head. Position and coverage estimated.").withId("sprinkler:zone-4-5"),
|
||||||
|
head(4, 6, "Rear path rotator", 93, 29, "rotator", 15, 90, 90,
|
||||||
|
"Confirmed zone 4 rotator. Position and coverage estimated."),
|
||||||
|
head(4, 7, "South fence west", 86, 50, "rotator", 15, 0, 90),
|
||||||
|
head(4, 8, "South fence by garage", 112, 49.5, "rotator", 15, 270, 90),
|
||||||
];
|
];
|
||||||
+31
-2
@@ -1,4 +1,6 @@
|
|||||||
import { defineGroup, Driveway, Walkway } from "../lib/yard-features.js";
|
import { defineGroup, Driveway, Street, Walkway } from "../lib/yard-features.js";
|
||||||
|
import * as geo from "../lib/geometry.js";
|
||||||
|
import { lotCorner } from "./anchors.js";
|
||||||
|
|
||||||
export const pavedSurfacesGroup = defineGroup("group:paved-surfaces", "Paved Surfaces", {
|
export const pavedSurfacesGroup = defineGroup("group:paved-surfaces", "Paved Surfaces", {
|
||||||
categoryId: "structures",
|
categoryId: "structures",
|
||||||
@@ -52,5 +54,32 @@ const stepPad = new Walkway("Step pad").inGroup(pavedSurfacesGroup).trace([
|
|||||||
[-100.89837111222856, 46.82674500346631],
|
[-100.89837111222856, 46.82674500346631],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const pavementFeatures = [frontWalk, driveway, garageWalk, stepPad];
|
// The front fence runs along the west edge of the 51-foot-deep lot.
|
||||||
|
// Put the sidewalk outside it, with roughly 15 feet beyond either end.
|
||||||
|
const frontSidewalk = new Walkway("Front sidewalk")
|
||||||
|
.inGroup(pavedSurfacesGroup)
|
||||||
|
.add(geo.offset(lotCorner), geo.north(geo.ft(15)))
|
||||||
|
.add(geo.offset(lotCorner), geo.north(geo.ft(15)), geo.west(geo.ft(4.5)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(51 + 15)), geo.west(geo.ft(4.5)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(51 + 15)));
|
||||||
|
|
||||||
|
// Near curb is 19 feet outside the fence; the far curb is another 40 feet west.
|
||||||
|
const frontStreet = new Street("6th Avenue NW")
|
||||||
|
.inGroup(pavedSurfacesGroup)
|
||||||
|
.add(geo.offset(lotCorner), geo.north(geo.ft(15)), geo.west(geo.ft(19)))
|
||||||
|
.add(geo.offset(lotCorner), geo.north(geo.ft(15)), geo.west(geo.ft(19 + 40)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(51 + 15)), geo.west(geo.ft(19 + 40)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(51 + 15)), geo.west(geo.ft(19)));
|
||||||
|
|
||||||
|
// The surveyed front walk meets the fence about 13.15 feet south of lotCorner.
|
||||||
|
// Center the 3-foot walk there, spanning the boulevard from sidewalk to curb.
|
||||||
|
const frontWalkCenterSouthFeet = 13.15;
|
||||||
|
const boulevardWalk = new Walkway("Boulevard walk")
|
||||||
|
.inGroup(pavedSurfacesGroup)
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(frontWalkCenterSouthFeet - 1.5)), geo.west(geo.ft(4.5)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(frontWalkCenterSouthFeet - 1.5)), geo.west(geo.ft(19)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(frontWalkCenterSouthFeet + 1.5)), geo.west(geo.ft(19)))
|
||||||
|
.add(geo.offset(lotCorner), geo.south(geo.ft(frontWalkCenterSouthFeet + 1.5)), geo.west(geo.ft(4.5)));
|
||||||
|
|
||||||
|
export const pavementFeatures = [frontWalk, driveway, garageWalk, stepPad, frontSidewalk, frontStreet, boulevardWalk];
|
||||||
export const pavementGroups = [pavedSurfacesGroup];
|
export const pavementGroups = [pavedSurfacesGroup];
|
||||||
@@ -3,19 +3,16 @@
|
|||||||
"nodes": [
|
"nodes": [
|
||||||
"visibility:category:plants",
|
"visibility:category:plants",
|
||||||
"visibility:category:plants>visibility:type:flower",
|
"visibility:category:plants>visibility:type:flower",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:83",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:84",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:85",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:86",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:86",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:87",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:87",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:88",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:88",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:89",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:89",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:90",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:90",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:feature:91",
|
"visibility:category:plants>visibility:type:flower>visibility:feature:91",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:feature:92",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:feature:93",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:feature:94",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:53",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:54",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:55",
|
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:56",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:56",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:57",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:57",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:58",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:58",
|
||||||
@@ -43,10 +40,10 @@
|
|||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:80",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:80",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:81",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:81",
|
||||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:82",
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:82",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:83",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:84",
|
||||||
|
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:85",
|
||||||
"visibility:category:plants>visibility:type:tree",
|
"visibility:category:plants>visibility:type:tree",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:92",
|
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:93",
|
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:94",
|
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:95",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:95",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:96",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:96",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:97",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:97",
|
||||||
@@ -56,31 +53,34 @@
|
|||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:101",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:101",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:102",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:102",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:103",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:103",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:114",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:104",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:115",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:105",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:116",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:106",
|
||||||
"visibility:category:plants>visibility:type:tree>visibility:feature:117",
|
"visibility:category:plants>visibility:type:tree>visibility:feature:117",
|
||||||
|
"visibility:category:plants>visibility:type:tree>visibility:feature:118",
|
||||||
|
"visibility:category:plants>visibility:type:tree>visibility:feature:119",
|
||||||
|
"visibility:category:plants>visibility:type:tree>visibility:feature:120",
|
||||||
"visibility:category:plants>visibility:type:bush",
|
"visibility:category:plants>visibility:type:bush",
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:feature:109",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:feature:110",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:feature:111",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:feature:112",
|
"visibility:category:plants>visibility:type:bush>visibility:feature:112",
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:feature:113",
|
"visibility:category:plants>visibility:type:bush>visibility:feature:113",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:feature:114",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:feature:115",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:feature:116",
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees",
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees",
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:104",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:105",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:106",
|
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:107",
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:107",
|
||||||
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:108",
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:108",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:109",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:110",
|
||||||
|
"visibility:category:plants>visibility:type:bush>visibility:group:juniper-trees>visibility:feature:111",
|
||||||
"visibility:category:structures",
|
"visibility:category:structures",
|
||||||
"visibility:category:structures>visibility:type:fence",
|
"visibility:category:structures>visibility:type:fence",
|
||||||
"visibility:category:structures>visibility:type:fence>visibility:feature:1",
|
"visibility:category:structures>visibility:type:fence>visibility:feature:1",
|
||||||
"visibility:category:structures>visibility:type:flowerBed",
|
"visibility:category:structures>visibility:type:flowerBed",
|
||||||
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:48",
|
|
||||||
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:49",
|
|
||||||
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:50",
|
|
||||||
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:51",
|
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:51",
|
||||||
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:52",
|
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:52",
|
||||||
|
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:53",
|
||||||
|
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:54",
|
||||||
|
"visibility:category:structures>visibility:type:flowerBed>visibility:feature:55",
|
||||||
"visibility:category:structures>visibility:group:buildings",
|
"visibility:category:structures>visibility:group:buildings",
|
||||||
"visibility:category:structures>visibility:group:buildings>visibility:feature:40",
|
"visibility:category:structures>visibility:group:buildings>visibility:feature:40",
|
||||||
"visibility:category:structures>visibility:group:buildings>visibility:feature:41",
|
"visibility:category:structures>visibility:group:buildings>visibility:feature:41",
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:46",
|
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:46",
|
||||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:47",
|
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:47",
|
||||||
"visibility:category:infrastructure",
|
"visibility:category:infrastructure",
|
||||||
"visibility:category:infrastructure>visibility:type:sprinkler",
|
"visibility:category:infrastructure>visibility:group:irrigation",
|
||||||
"visibility:category:infrastructure>visibility:type:sprinkler>visibility:feature:118",
|
null,
|
||||||
"visibility:category:infrastructure>visibility:type:sprinkler>visibility:feature:119",
|
null,
|
||||||
"visibility:category:reference",
|
"visibility:category:reference",
|
||||||
"visibility:category:reference>visibility:type:lotBoundary",
|
"visibility:category:reference>visibility:type:lotBoundary",
|
||||||
"visibility:category:reference>visibility:type:lotBoundary>visibility:feature:0",
|
"visibility:category:reference>visibility:type:lotBoundary>visibility:feature:0",
|
||||||
@@ -137,6 +137,45 @@
|
|||||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:36",
|
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:36",
|
||||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:37",
|
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:37",
|
||||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:38",
|
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:38",
|
||||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:39"
|
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:39",
|
||||||
|
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:48",
|
||||||
|
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:49",
|
||||||
|
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:50",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:121",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:122",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:123",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:124",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:125",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:126",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:127",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:128",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-1>visibility:feature:129",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:130",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:131",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:132",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:133",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:134",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:135",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:136",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:137",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:138",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:139",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:140",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:141",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:142",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:143",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-3>visibility:feature:144",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:145",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:146",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:147",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:148",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-2>visibility:feature:149",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:150",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:151",
|
||||||
|
"visibility:category:infrastructure>visibility:group:irrigation>visibility:group:irrigation-zone-4>visibility:feature:152"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -2,7 +2,7 @@ import { collectYardData } from "../lib/yard-features.js";
|
|||||||
import { boundaryFeatures } from "./boundaries.js";
|
import { boundaryFeatures } from "./boundaries.js";
|
||||||
import { buildingFeatures, buildingGroups } from "./buildings.js";
|
import { buildingFeatures, buildingGroups } from "./buildings.js";
|
||||||
import { gridFeatures, gridGroups } from "./grid.js";
|
import { gridFeatures, gridGroups } from "./grid.js";
|
||||||
import { irrigationFeatures } from "./irrigation.js";
|
import { irrigationFeatures, irrigationGroups } from "./irrigation.js";
|
||||||
import { pavementFeatures, pavementGroups } from "./pavement.js";
|
import { pavementFeatures, pavementGroups } from "./pavement.js";
|
||||||
import { plantFeatures, plantGroups } from "./plants.js";
|
import { plantFeatures, plantGroups } from "./plants.js";
|
||||||
|
|
||||||
@@ -20,5 +20,6 @@ export const yardGeoJSON = collectYardData(
|
|||||||
...gridGroups,
|
...gridGroups,
|
||||||
...pavementGroups,
|
...pavementGroups,
|
||||||
...plantGroups,
|
...plantGroups,
|
||||||
|
...irrigationGroups,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -124,6 +124,19 @@ export const featureTypes = {
|
|||||||
fillOpacity: 0.8,
|
fillOpacity: 0.8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
street: {
|
||||||
|
label: "Street",
|
||||||
|
collectionLabel: "Streets",
|
||||||
|
categoryId: "structures",
|
||||||
|
geometryType: "Polygon",
|
||||||
|
detailFields: {},
|
||||||
|
style: {
|
||||||
|
color: "#62666b",
|
||||||
|
weight: 2,
|
||||||
|
fillColor: "#92979d",
|
||||||
|
fillOpacity: 0.45,
|
||||||
|
},
|
||||||
|
},
|
||||||
flowerBed: {
|
flowerBed: {
|
||||||
label: "Flower Bed",
|
label: "Flower Bed",
|
||||||
collectionLabel: "Flower Beds",
|
collectionLabel: "Flower Beds",
|
||||||
@@ -279,13 +292,23 @@ export const featureTypes = {
|
|||||||
zone: "string",
|
zone: "string",
|
||||||
headType: "string",
|
headType: "string",
|
||||||
flowRateGallonsPerMinute: "number",
|
flowRateGallonsPerMinute: "number",
|
||||||
|
reachFeet: "number",
|
||||||
|
startBearingDegrees: "number",
|
||||||
|
arcDegrees: "number",
|
||||||
|
coverageEstimated: "boolean",
|
||||||
|
note: "string",
|
||||||
},
|
},
|
||||||
style: {
|
style: (details) => ({
|
||||||
color: "#2d7dd2",
|
color: details.headType === "mist" ? "#087f8c" : "#3457b2",
|
||||||
radius: 5,
|
radius: 5,
|
||||||
fillColor: "#88bdf2",
|
fillColor: "#88bdf2",
|
||||||
fillOpacity: 0.95,
|
fillOpacity: 0.95,
|
||||||
},
|
...(details.headType === "mist" || details.headType === "rotator" ? {
|
||||||
|
iconUrl: `./src/assets/icons/sprinkler-${details.headType}.svg`,
|
||||||
|
iconOpacity: 0.2,
|
||||||
|
iconSize: [28, 28],
|
||||||
|
} : {}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
bush: {
|
bush: {
|
||||||
label: "Bush",
|
label: "Bush",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { east, ft, north, offset, point } from "./geometry.js";
|
||||||
|
|
||||||
|
// Bearings are clockwise from true north. Add the anchor's rotation when
|
||||||
|
// authoring a bearing relative to the lot. Reach is a radius, in feet.
|
||||||
|
// Missing coverage stays hidden until the head has been located and estimated.
|
||||||
|
export function sprinklerCoverage(feature) {
|
||||||
|
const { reachFeet, startBearingDegrees = 0, arcDegrees = 360 } = feature.properties.details;
|
||||||
|
if (!Number.isFinite(reachFeet) || reachFeet <= 0) return null;
|
||||||
|
if (!Number.isFinite(startBearingDegrees) || !Number.isFinite(arcDegrees) ||
|
||||||
|
arcDegrees <= 0 || arcDegrees > 360) return null;
|
||||||
|
const [lon, lat] = feature.geometry.coordinates;
|
||||||
|
const origin = point(lat, lon);
|
||||||
|
const steps = Math.ceil(arcDegrees / 5);
|
||||||
|
const ring = arcDegrees === 360 ? [] : [[lon, lat]];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const angle = (startBearingDegrees + arcDegrees * i / steps) * Math.PI / 180;
|
||||||
|
const tip = offset(origin, north(ft(reachFeet) * Math.cos(angle)),
|
||||||
|
east(ft(reachFeet) * Math.sin(angle)));
|
||||||
|
ring.push([tip.lon, tip.lat]);
|
||||||
|
}
|
||||||
|
ring.push(ring[0]);
|
||||||
|
return ring;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sprinklerCoverageStyle(details) {
|
||||||
|
return {
|
||||||
|
color: details.headType === "mist" ? "#087f8c" : "#3457b2",
|
||||||
|
weight: 1,
|
||||||
|
opacity: 0.65,
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
dashArray: details.headType === "mist" ? "2 4" : "7 4",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { sprinklerCoverage, sprinklerCoverageStyle } from "./irrigation.js";
|
||||||
|
import { resolveFeatureStyle } from "./feature-types.js";
|
||||||
|
|
||||||
|
const WIDTH = 1000;
|
||||||
|
const HEIGHT = 750;
|
||||||
|
const PADDING = 24;
|
||||||
|
const FEET_PER_METER = 3.280839895;
|
||||||
|
|
||||||
|
// Inverse of offset()'s anchor rotation: local north is up on paper.
|
||||||
|
export function projectPrintCoordinate([lon, lat], anchor) {
|
||||||
|
const angle = (anchor.assumedNorthClockwiseDegrees ?? 0) * Math.PI / 180;
|
||||||
|
const north = (lat - anchor.lat) * 111320;
|
||||||
|
const east = (lon - anchor.lon) * 111320 * Math.cos(anchor.lat * Math.PI / 180);
|
||||||
|
return [east * Math.cos(angle) - north * Math.sin(angle),
|
||||||
|
-(north * Math.cos(angle) + east * Math.sin(angle))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function svgElement(name, attributes = {}, text) {
|
||||||
|
const element = document.createElementNS("http://www.w3.org/2000/svg", name);
|
||||||
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
|
if (value !== undefined) element.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
if (text !== undefined) element.textContent = text;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPrintMap(collection, fullCollection, anchor, date) {
|
||||||
|
const project = (coordinate) => projectPrintCoordinate(coordinate, anchor);
|
||||||
|
const points = [];
|
||||||
|
const visit = (coordinates) => {
|
||||||
|
if (typeof coordinates[0] === "number") points.push(project(coordinates));
|
||||||
|
else coordinates.forEach(visit);
|
||||||
|
};
|
||||||
|
// Keep the full yard in view even when reference features are unchecked.
|
||||||
|
// The authored grid extends well beyond the yard and must not shrink the map.
|
||||||
|
for (const feature of fullCollection.features) {
|
||||||
|
if (feature.properties.kind !== "yardGrid") visit(feature.geometry.coordinates);
|
||||||
|
}
|
||||||
|
const xs = points.map(([x]) => x);
|
||||||
|
const ys = points.map(([, y]) => y);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
const scale = Math.min((WIDTH - 2 * PADDING) / (maxX - minX || 1),
|
||||||
|
(HEIGHT - 2 * PADDING) / (maxY - minY || 1));
|
||||||
|
const screen = (coordinate) => {
|
||||||
|
const [x, y] = project(coordinate);
|
||||||
|
return [WIDTH / 2 + (x - (minX + maxX) / 2) * scale,
|
||||||
|
HEIGHT / 2 + (y - (minY + maxY) / 2) * scale];
|
||||||
|
};
|
||||||
|
const svg = svgElement("svg", {
|
||||||
|
viewBox: `0 0 ${WIDTH} ${HEIGHT}`, role: "img",
|
||||||
|
"aria-label": `Yard sketch map for ${date}, aligned with the lot`,
|
||||||
|
});
|
||||||
|
svg.append(svgElement("title", {}, `204 6th Avenue NW — ${date}`));
|
||||||
|
const defs = svgElement("defs");
|
||||||
|
const clip = svgElement("clipPath", { id: "print-map-clip" });
|
||||||
|
clip.append(svgElement("rect", { x: PADDING, y: PADDING,
|
||||||
|
width: WIDTH - 2 * PADDING, height: HEIGHT - 2 * PADDING }));
|
||||||
|
defs.append(clip);
|
||||||
|
svg.append(defs);
|
||||||
|
const features = svgElement("g", { "clip-path": "url(#print-map-clip)" });
|
||||||
|
// Draw the grid first so it doesn't cover plants or structures.
|
||||||
|
const ordered = [...collection.features].sort((a, b) =>
|
||||||
|
Number(b.properties.kind === "yardGrid") - Number(a.properties.kind === "yardGrid"));
|
||||||
|
for (const feature of ordered) {
|
||||||
|
if (feature.properties.kind !== "sprinkler") continue;
|
||||||
|
const ring = sprinklerCoverage(feature);
|
||||||
|
if (!ring) continue;
|
||||||
|
const style = sprinklerCoverageStyle(feature.properties.details);
|
||||||
|
features.append(svgElement("path", {
|
||||||
|
d: ring.map((coordinate, i) => `${i ? "L" : "M"}${screen(coordinate).join(" ")}`).join(" ") + " Z",
|
||||||
|
stroke: style.color, "stroke-width": style.weight,
|
||||||
|
"stroke-dasharray": style.dashArray, "stroke-opacity": style.opacity,
|
||||||
|
fill: style.color, "fill-opacity": style.fillOpacity,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (const feature of ordered) {
|
||||||
|
const style = resolveFeatureStyle(feature.properties.kind, feature.properties.details);
|
||||||
|
const { type, coordinates } = feature.geometry;
|
||||||
|
const attributes = {
|
||||||
|
stroke: style.color ?? "#333", "stroke-width": Math.max(0.8, (style.weight ?? 2) * 0.65),
|
||||||
|
"stroke-opacity": style.opacity ?? 1,
|
||||||
|
"stroke-dasharray": style.dashArray,
|
||||||
|
fill: type === "LineString" ? "none" : (style.fillColor ?? style.color ?? "#333"),
|
||||||
|
// Light fills leave pencil marks easy to see; outlines retain type colors.
|
||||||
|
"fill-opacity": Math.min(style.fillOpacity ?? 0.2, 0.18),
|
||||||
|
"stroke-linejoin": "round", "stroke-linecap": "round",
|
||||||
|
};
|
||||||
|
let shape;
|
||||||
|
if (type === "Point") {
|
||||||
|
const [cx, cy] = screen(coordinates);
|
||||||
|
if (feature.properties.kind === "sprinkler" && style.iconUrl) {
|
||||||
|
const icon = svgElement("image", {
|
||||||
|
href: style.iconUrl, x: cx - 9, y: cy - 9, width: 18, height: 18,
|
||||||
|
opacity: style.iconOpacity ?? 1,
|
||||||
|
});
|
||||||
|
icon.append(svgElement("title", {}, feature.properties.name));
|
||||||
|
features.append(icon);
|
||||||
|
if (feature.properties.details.zone) {
|
||||||
|
features.append(svgElement("text", {
|
||||||
|
x: cx + 10, y: cy + 4, fill: "#234254", "font-size": 10,
|
||||||
|
"font-family": "sans-serif", stroke: "white", "stroke-width": 3,
|
||||||
|
"paint-order": "stroke",
|
||||||
|
}, feature.properties.details.zone));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const radius = style.radiusMeters ?? (style.iconDiameterMeters !== undefined
|
||||||
|
? style.iconDiameterMeters / 2 : undefined);
|
||||||
|
shape = svgElement("circle", { ...attributes, cx, cy,
|
||||||
|
r: radius !== undefined ? Math.max(1.5, radius * scale) : (style.radius ?? 5) * 0.65 });
|
||||||
|
} else {
|
||||||
|
const rings = type === "Polygon" ? coordinates : [coordinates];
|
||||||
|
const d = rings.map((ring) => ring.map((coordinate, index) =>
|
||||||
|
`${index ? "L" : "M"}${screen(coordinate).join(" ")}`).join(" ") +
|
||||||
|
(type === "Polygon" ? " Z" : "")).join(" ");
|
||||||
|
shape = svgElement("path", { ...attributes, d, "fill-rule": "evenodd" });
|
||||||
|
}
|
||||||
|
shape.append(svgElement("title", {}, feature.properties.name));
|
||||||
|
features.append(shape);
|
||||||
|
}
|
||||||
|
svg.append(features);
|
||||||
|
const label = svgElement("g", { fill: "#222", "font-family": "sans-serif", "font-size": 12 });
|
||||||
|
label.append(svgElement("rect", { x: 20, y: 4, width: 310, height: 22, fill: "white" }));
|
||||||
|
label.append(svgElement("text", { x: 24, y: 19 }, `204 6th Avenue NW · Map date ${date}`));
|
||||||
|
if (collection.features.some((feature) => feature.properties.kind === "sprinkler")) {
|
||||||
|
label.append(svgElement("rect", { x: WIDTH - 330, y: HEIGHT - 36,
|
||||||
|
width: 310, height: 30, fill: "white" }));
|
||||||
|
for (const [index, type] of ["mist", "rotator"].entries()) {
|
||||||
|
const x = WIDTH - 325 + index * 82;
|
||||||
|
label.append(svgElement("image", { href: `./src/assets/icons/sprinkler-${type}.svg`,
|
||||||
|
x, y: HEIGHT - 32, width: 18, height: 18 }));
|
||||||
|
label.append(svgElement("text", { x: x + 21, y: HEIGHT - 19 },
|
||||||
|
type === "mist" ? "Mist" : "Rotator"));
|
||||||
|
}
|
||||||
|
label.append(svgElement("text", { x: WIDTH - 158, y: HEIGHT - 19,
|
||||||
|
"font-size": 10 }, "Coverage estimated"));
|
||||||
|
}
|
||||||
|
const bar = 20 / FEET_PER_METER * scale;
|
||||||
|
label.append(svgElement("rect", { x: 20, y: HEIGHT - 40, width: bar + 16, height: 36, fill: "white" }));
|
||||||
|
label.append(svgElement("path", { d: `M24 ${HEIGHT - 24} v8 h${bar} v-8`, fill: "none", stroke: "#222", "stroke-width": 2 }));
|
||||||
|
label.append(svgElement("text", { x: 24 + bar / 2, y: HEIGHT - 25, "text-anchor": "middle" }, "20 ft"));
|
||||||
|
const north = svgElement("g", { transform: `translate(${WIDTH - 55} 65)` });
|
||||||
|
north.append(svgElement("circle", { r: 42, fill: "white" }));
|
||||||
|
const arrow = svgElement("g", { transform: `rotate(${-(anchor.assumedNorthClockwiseDegrees ?? 0)})` });
|
||||||
|
arrow.append(svgElement("path", { d: "M0 25 V-20 M-6 -10 L0 -25 L6 -10", fill: "none", stroke: "#222", "stroke-width": 2 }));
|
||||||
|
arrow.append(svgElement("text", { x: 0, y: -30, "text-anchor": "middle" }, "N"));
|
||||||
|
north.append(arrow);
|
||||||
|
label.append(north);
|
||||||
|
svg.append(label);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
@@ -284,6 +284,12 @@ export class Driveway extends PolygonFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Street extends PolygonFeature {
|
||||||
|
constructor(name = "Street", details = {}) {
|
||||||
|
super("street", name, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FlowerBed extends PolygonFeature {
|
export class FlowerBed extends PolygonFeature {
|
||||||
constructor(name = "Flower Bed", details = {}) {
|
constructor(name = "Flower Bed", details = {}) {
|
||||||
super("flowerBed", name, details);
|
super("flowerBed", name, details);
|
||||||
|
|||||||
+97
-2
@@ -1,12 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
Map as MapLibreMap,
|
Map as MapLibreMap,
|
||||||
NavigationControl,
|
NavigationControl,
|
||||||
|
Marker,
|
||||||
Popup,
|
Popup,
|
||||||
addProtocol,
|
addProtocol,
|
||||||
} from "../vendor/maplibre/maplibre-gl.mjs";
|
} from "../vendor/maplibre/maplibre-gl.mjs";
|
||||||
import { yardGeoJSON } from "./data/yard.js";
|
import { yardGeoJSON } from "./data/yard.js";
|
||||||
import { CheckboxTree, loadManifest } from "../vendor/checkbox-tree/checkbox-tree.js";
|
import { CheckboxTree, loadManifest } from "../vendor/checkbox-tree/checkbox-tree.js";
|
||||||
import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";
|
import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";
|
||||||
|
import { lotCorner } from "./data/anchors.js";
|
||||||
|
import { sprinklerCoverage, sprinklerCoverageStyle } from "./lib/irrigation.js";
|
||||||
|
import { buildPrintMap } from "./lib/print-map.js";
|
||||||
|
|
||||||
const MAP_MAX_ZOOM = 24;
|
const MAP_MAX_ZOOM = 24;
|
||||||
const FEET_PER_METER = 3.280839895;
|
const FEET_PER_METER = 3.280839895;
|
||||||
@@ -52,6 +56,7 @@ map.addControl(new NavigationControl(), "bottom-right");
|
|||||||
|
|
||||||
let selectedTimelineDate = TIMELINE_PRESETS.at(-1).date;
|
let selectedTimelineDate = TIMELINE_PRESETS.at(-1).date;
|
||||||
let selectedFeatureIndexes = new Set(yardGeoJSON.features.map((_, index) => index));
|
let selectedFeatureIndexes = new Set(yardGeoJSON.features.map((_, index) => index));
|
||||||
|
let renderedMarkers = [];
|
||||||
let renderedLayerIds = [];
|
let renderedLayerIds = [];
|
||||||
let renderedSourceIds = [];
|
let renderedSourceIds = [];
|
||||||
let renderedLayerHandlers = [];
|
let renderedLayerHandlers = [];
|
||||||
@@ -80,6 +85,24 @@ map.on("error", (event) => {
|
|||||||
statusElement.hidden = false;
|
statusElement.hidden = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function filterFeatureCollection(collection, date, visibleIndexes) {
|
||||||
|
return { ...collection, features: collection.features.filter(
|
||||||
|
(feature, index) => visibleIndexes.has(index) && featureExistsOnDate(feature, date),
|
||||||
|
) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePrintMap() {
|
||||||
|
document.getElementById("print-sheet").replaceChildren(buildPrintMap(
|
||||||
|
filterFeatureCollection(yardGeoJSON, selectedTimelineDate, selectedFeatureIndexes),
|
||||||
|
yardGeoJSON, lotCorner, selectedTimelineDate,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
window.addEventListener("beforeprint", preparePrintMap);
|
||||||
|
document.getElementById("print-map").addEventListener("click", () => {
|
||||||
|
preparePrintMap();
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
|
||||||
function renderYardForDate(date) {
|
function renderYardForDate(date) {
|
||||||
removeRenderedYard();
|
removeRenderedYard();
|
||||||
const features = yardGeoJSON.features.filter(
|
const features = yardGeoJSON.features.filter(
|
||||||
@@ -92,6 +115,8 @@ function renderYardForDate(date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeRenderedYard() {
|
function removeRenderedYard() {
|
||||||
|
for (const marker of renderedMarkers) marker.remove();
|
||||||
|
renderedMarkers = [];
|
||||||
for (const { layerId, event, handler } of renderedLayerHandlers) {
|
for (const { layerId, event, handler } of renderedLayerHandlers) {
|
||||||
map.off(event, layerId, handler);
|
map.off(event, layerId, handler);
|
||||||
}
|
}
|
||||||
@@ -111,6 +136,10 @@ function removeRenderedYard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addFeatureLayer(feature, index) {
|
function addFeatureLayer(feature, index) {
|
||||||
|
if (feature.properties.kind === "sprinkler") {
|
||||||
|
addSprinkler(feature, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sourceId = `yard-source-${index}`;
|
const sourceId = `yard-source-${index}`;
|
||||||
const layerId = `yard-layer-${index}`;
|
const layerId = `yard-layer-${index}`;
|
||||||
const style = resolveFeatureStyle(feature.properties.kind, feature.properties.details);
|
const style = resolveFeatureStyle(feature.properties.kind, feature.properties.details);
|
||||||
@@ -148,6 +177,57 @@ function addFeatureLayer(feature, index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSprinkler(feature, index) {
|
||||||
|
const details = feature.properties.details;
|
||||||
|
const style = resolveFeatureStyle("sprinkler", details);
|
||||||
|
const ring = sprinklerCoverage(feature);
|
||||||
|
if (ring) {
|
||||||
|
const source = `sprinkler-coverage-${index}`;
|
||||||
|
const coverageStyle = sprinklerCoverageStyle(details);
|
||||||
|
map.addSource(source, { type: "geojson", data: {
|
||||||
|
type: "Feature", properties: {},
|
||||||
|
geometry: { type: "Polygon", coordinates: [ring] },
|
||||||
|
} });
|
||||||
|
renderedSourceIds.push(source);
|
||||||
|
for (const layer of [
|
||||||
|
{ id: `${source}-fill`, source, type: "fill", paint: {
|
||||||
|
"fill-color": coverageStyle.color, "fill-opacity": coverageStyle.fillOpacity,
|
||||||
|
} },
|
||||||
|
{ id: `${source}-line`, source, type: "line", paint: {
|
||||||
|
"line-color": coverageStyle.color, "line-opacity": coverageStyle.opacity,
|
||||||
|
"line-width": coverageStyle.weight,
|
||||||
|
"line-dasharray": coverageStyle.dashArray.split(/\s+/).map(Number),
|
||||||
|
} },
|
||||||
|
]) {
|
||||||
|
map.addLayer(layer);
|
||||||
|
renderedLayerIds.push(layer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const element = document.createElement("div");
|
||||||
|
element.style.cursor = "pointer";
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.src = style.iconUrl;
|
||||||
|
image.alt = feature.properties.name;
|
||||||
|
image.width = 28;
|
||||||
|
image.height = 28;
|
||||||
|
image.style.display = "block";
|
||||||
|
image.style.opacity = style.iconOpacity ?? 1;
|
||||||
|
element.append(image);
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "sprinkler-zone-label";
|
||||||
|
label.textContent = `Zone ${details.zone}`;
|
||||||
|
label.style.position = "absolute";
|
||||||
|
label.style.whiteSpace = "nowrap";
|
||||||
|
label.style.left = "50%";
|
||||||
|
label.style.transform = "translateX(-50%)";
|
||||||
|
element.append(label);
|
||||||
|
const marker = new Marker({ element })
|
||||||
|
.setLngLat(feature.geometry.coordinates)
|
||||||
|
.setPopup(new Popup({ offset: 16 }).setHTML(buildPopupContent(feature.properties)))
|
||||||
|
.addTo(map);
|
||||||
|
renderedMarkers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
function buildMapLibreLayer(id, source, feature, style) {
|
function buildMapLibreLayer(id, source, feature, style) {
|
||||||
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
|
||||||
return {
|
return {
|
||||||
@@ -550,6 +630,13 @@ function shouldShowDetailField(key, details = {}) {
|
|||||||
|
|
||||||
function formatDetailLabel(key) {
|
function formatDetailLabel(key) {
|
||||||
const labels = {
|
const labels = {
|
||||||
|
zone: "Zone",
|
||||||
|
headType: "Head type",
|
||||||
|
reachFeet: "Reach (radius)",
|
||||||
|
startBearingDegrees: "Start bearing (true north)",
|
||||||
|
arcDegrees: "Spray arc",
|
||||||
|
coverageEstimated: "Estimated coverage",
|
||||||
|
flowRateGallonsPerMinute: "Flow (gal/min)",
|
||||||
canopyDiameter: "Canopy diameter",
|
canopyDiameter: "Canopy diameter",
|
||||||
height: "Height",
|
height: "Height",
|
||||||
measurements: "Measurements",
|
measurements: "Measurements",
|
||||||
@@ -574,8 +661,16 @@ function formatDetailLabel(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDetailValue(key, value) {
|
function formatDetailValue(key, value) {
|
||||||
if (key === "canopyDiameter" || key === "height") return formatMetersAsFeet(value);
|
if (key === "canopyDiameter" || key === "height") {
|
||||||
if (key === "offsetFeet" || key === "spacingFeet") return `${value} ft`;
|
return formatMetersAsFeet(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "coverageEstimated") return value ? "Yes" : "No";
|
||||||
|
if (key === "startBearingDegrees" || key === "arcDegrees") return `${value}°`;
|
||||||
|
if (key === "reachFeet" || key === "offsetFeet" || key === "spacingFeet") {
|
||||||
|
return `${value} ft`;
|
||||||
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+87
@@ -386,3 +386,90 @@ main,
|
|||||||
padding: 0.55rem;
|
padding: 0.55rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#print-map {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#print-map:focus-visible {
|
||||||
|
outline: 2px solid var(--ink);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-hint {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#print-sheet {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: landscape;
|
||||||
|
margin: 0.25in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell,
|
||||||
|
.error-overlay {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#print-sheet {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
#print-sheet svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprinkler-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprinkler-legend span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprinkler-legend img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprinkler-zone-label {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
padding: 0 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #234254;
|
||||||
|
}
|
||||||
|
.sprinkler-zone-label::before { display: none; }
|
||||||
Reference in new issue
Block a user