Add printable yard map and sprinkler zones with estimated coverage
Check and deploy / deploy (push) Successful in 25s

This commit is contained in:
aaron.axvig committed 2026-10-14 21:24:39 -05:00
1 parent c8c26c1706
commit 6fd308aca8
14 files changed
+609 -37

No files matched your search

+14
View File
@@ -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.
+7
View File
@@ -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>
+10
View File
@@ -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

+8
View File
@@ -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

+70 -4
View File
@@ -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
View File
@@ -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];
+64 -25
View File
@@ -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
View File
@@ -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,
], ],
); );
+26 -3
View File
@@ -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",
+33
View File
@@ -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",
};
}
+154
View File
@@ -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;
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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; }