Merge remote-tracking branch 'origin/main'
# Conflicts: # AGENTS.md # README.md # index.html # package.json # src/data/buildings.js # src/data/grid.js # src/data/pavement.js # src/data/plants.js # src/data/yard.js # src/lib/feature-types.js # src/lib/yard-features.js # src/main.js # styles.css
This commit is contained in:
+10
-4
@@ -1,8 +1,14 @@
|
||||
import { Deck, defineGroup, Garage, House, Porch } from "../lib/yard-features.js";
|
||||
|
||||
export const housePartsGroup = defineGroup("group:house-parts", "House Parts");
|
||||
export const buildingsGroup = defineGroup("group:buildings", "Buildings", {
|
||||
categoryId: "structures",
|
||||
});
|
||||
export const housePartsGroup = defineGroup("group:house-parts", "House Parts", {
|
||||
categoryId: "structures",
|
||||
parentGroupId: buildingsGroup.id,
|
||||
});
|
||||
|
||||
const house = new House().withId("feature:house-main").trace([
|
||||
const house = new House().withId("feature:house-main").inGroup(buildingsGroup).trace([
|
||||
[-100.89883475400929, 46.826808023423844],
|
||||
[-100.8986668040935, 46.826830709973976],
|
||||
[-100.89866031675769, 46.826807530237865],
|
||||
@@ -11,7 +17,7 @@ const house = new House().withId("feature:house-main").trace([
|
||||
[-100.89881168792644, 46.82673552503637],
|
||||
]);
|
||||
|
||||
const garage = new Garage().trace([
|
||||
const garage = new Garage().inGroup(buildingsGroup).trace([
|
||||
[-100.89850173743805, 46.82679569377302],
|
||||
[-100.89839289435957, 46.82681098253964],
|
||||
[-100.89837054909181, 46.82674095008918],
|
||||
@@ -39,4 +45,4 @@ const deck = new Deck().childOf(house).inGroup(housePartsGroup).trace([
|
||||
]);
|
||||
|
||||
export const buildingFeatures = [house, garage, porch, deck];
|
||||
export const buildingGroups = [housePartsGroup];
|
||||
export const buildingGroups = [buildingsGroup, housePartsGroup];
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ const MAX_EAST_FEET = 180;
|
||||
const MIN_NORTH_FEET = -140;
|
||||
const MAX_NORTH_FEET = 20;
|
||||
|
||||
export const yardGridGroup = defineGroup("group:yard-grid", "Yard Grid");
|
||||
export const yardGridGroup = defineGroup("group:yard-grid", "Yard Grid", {
|
||||
categoryId: "reference",
|
||||
});
|
||||
|
||||
function localPoint(eastFeet, northFeet) {
|
||||
return geo.offset(
|
||||
|
||||
+11
-6
@@ -1,6 +1,10 @@
|
||||
import { Driveway, Walkway } from "../lib/yard-features.js";
|
||||
import { defineGroup, Driveway, Walkway } from "../lib/yard-features.js";
|
||||
|
||||
const frontWalk = new Walkway("Front walk").trace([
|
||||
export const pavedSurfacesGroup = defineGroup("group:paved-surfaces", "Paved Surfaces", {
|
||||
categoryId: "structures",
|
||||
});
|
||||
|
||||
const frontWalk = new Walkway("Front walk").inGroup(pavedSurfacesGroup).trace([
|
||||
[-100.89894913835376, 46.82676927748655],
|
||||
[-100.89887345276941, 46.82677975769509],
|
||||
[-100.89887849847504, 46.82679985503042],
|
||||
@@ -22,14 +26,14 @@ const frontWalk = new Walkway("Front walk").trace([
|
||||
[-100.8989459622624, 46.826758196205],
|
||||
]);
|
||||
|
||||
const driveway = new Driveway().trace([
|
||||
const driveway = new Driveway().inGroup(pavedSurfacesGroup).trace([
|
||||
[-100.89851482473702, 46.82685818966149],
|
||||
[-100.8984941013032, 46.826796541436565],
|
||||
[-100.89837336477572, 46.82681355635369],
|
||||
[-100.89839320971627, 46.8268766687031],
|
||||
]);
|
||||
|
||||
const garageWalk = new Walkway("Garage walk").trace([
|
||||
const garageWalk = new Walkway("Garage walk").inGroup(pavedSurfacesGroup).trace([
|
||||
[-100.89862628077022, 46.82677780036217],
|
||||
[-100.8986050167251, 46.82677755376906],
|
||||
[-100.89853023215954, 46.82678544474858],
|
||||
@@ -41,11 +45,12 @@ const garageWalk = new Walkway("Garage walk").trace([
|
||||
[-100.89862267669479, 46.82676559400101],
|
||||
]);
|
||||
|
||||
const stepPad = new Walkway("Step pad").trace([
|
||||
const stepPad = new Walkway("Step pad").inGroup(pavedSurfacesGroup).trace([
|
||||
[-100.89837579752665, 46.82675708653554],
|
||||
[-100.89835705633432, 46.826759675764286],
|
||||
[-100.89835309185132, 46.82674722280572],
|
||||
[-100.89837111222856, 46.82674500346631],
|
||||
]);
|
||||
|
||||
export const pavementFeatures = [frontWalk, driveway, garageWalk, stepPad];
|
||||
export const pavementFeatures = [frontWalk, driveway, garageWalk, stepPad];
|
||||
export const pavementGroups = [pavedSurfacesGroup];
|
||||
|
||||
+161
-74
@@ -1,8 +1,11 @@
|
||||
import { Daylily, defineGroup, Flower, FlowerBed, Tree } from "../lib/yard-features.js";
|
||||
import { Bush, Daylily, defineGroup, Flower, FlowerBed, Tree } from "../lib/yard-features.js";
|
||||
import * as geo from "../lib/geometry.js";
|
||||
import { lotCorner } from "./anchors.js";
|
||||
|
||||
export const juniperTreesGroup = defineGroup("group:juniper-trees", "Juniper Trees");
|
||||
export const juniperTreesGroup = defineGroup("group:juniper-trees", "Junipers", {
|
||||
categoryId: "plants",
|
||||
parentKind: "bush",
|
||||
});
|
||||
|
||||
const flowerBeds = [
|
||||
new FlowerBed("Herbs").trace([
|
||||
@@ -48,25 +51,47 @@ const flowerBeds = [
|
||||
];
|
||||
|
||||
const daylilies = [
|
||||
new Daylily("Not sure", {
|
||||
gardenOrgId: "4765",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(2))),
|
||||
new Daylily("Trahlyta", {
|
||||
gardenOrgId: "18613",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(1))),
|
||||
new Daylily("Wayside King Royale", {
|
||||
gardenOrgId: "5090",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(4))),
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(3))),
|
||||
new Daylily("Night Stalker", {
|
||||
gardenOrgId: "58861",
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 16,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(5))),
|
||||
new Daylily("Lemon Fringed Lemons", {
|
||||
gardenOrgId: "185536",
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 15,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(7))),
|
||||
new Daylily("Joan Derifield", {
|
||||
gardenOrgId: "4765",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(6))),
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 15,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(9))),
|
||||
new Daylily("Predatory Flamingo", {
|
||||
gardenOrgId: "61201",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(8))),
|
||||
new Daylily("Unknown", {
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 6,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(112)), geo.south(geo.ft(11))),
|
||||
new Daylily("Double Me", {
|
||||
gardenOrgId: "7872",
|
||||
plantedDate: "2022-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(109)), geo.south(geo.ft(7))),
|
||||
new Daylily("Unknown", {
|
||||
new Daylily("Lipstick Spitfire", {
|
||||
gardenOrgId: "708897",
|
||||
plantedDate: "2022-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(109)), geo.south(geo.ft(9))),
|
||||
new Daylily("Unknown", {
|
||||
new Daylily("Jerry Hyatt", {
|
||||
gardenOrgId: "32921",
|
||||
plantedDate: "2022-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(107)), geo.south(geo.ft(8))),
|
||||
new Daylily("Unknown", {
|
||||
new Daylily("Take Me Home", {
|
||||
gardenOrgId: "791256",
|
||||
plantedDate: "2022-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(107)), geo.south(geo.ft(10))),
|
||||
new Daylily("Orange Daylily", {
|
||||
gardenOrgId: "48484",
|
||||
@@ -92,13 +117,48 @@ const daylilies = [
|
||||
new Daylily("Orange Daylily", {
|
||||
gardenOrgId: "48484",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(142)), geo.south(geo.ft(38))),
|
||||
new Daylily("Extra daylily", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(0.8)), geo.south(geo.ft(3.2))),
|
||||
new Daylily("Extra daylily", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(0.8)), geo.south(geo.ft(3.9))),
|
||||
new Daylily("Extra daylily", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1.3)), geo.south(geo.ft(3.6))),
|
||||
new Daylily("Extra daylily", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(9))),
|
||||
new Daylily("Extra daylily", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(17))),
|
||||
new Daylily("Can't read", {
|
||||
gardenOrgId: "",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(25))),
|
||||
new Daylily("J.T. Davis", {
|
||||
gardenOrgId: "29649",
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 21,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(31))),
|
||||
new Daylily("Tea for Teagan", {
|
||||
gardenOrgId: "787574",
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 15,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(34))),
|
||||
new Daylily("Burning So Brightly", {
|
||||
gardenOrgId: "610042",
|
||||
plantedDate: "2023-05-01",
|
||||
pricePaid: 17,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(2)), geo.south(geo.ft(32.5))),
|
||||
new Daylily("You Had Me at Hello", {
|
||||
gardenOrgId: "235117",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(28)), geo.south(geo.ft(45))),
|
||||
new Daylily("Mapping North Dakota", {
|
||||
gardenOrgId: "56003",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(28)), geo.south(geo.ft(47))),
|
||||
new Daylily("Daylily").add(geo.offset(lotCorner), geo.east(geo.ft(31)), geo.south(geo.ft(47))),
|
||||
new Daylily("Monster", {
|
||||
gardenOrgId: "15661",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(31)), geo.south(geo.ft(47))),
|
||||
];
|
||||
|
||||
const flowers = [
|
||||
@@ -121,14 +181,14 @@ const flowers = [
|
||||
new Flower("Tiny Tortuga Turtlehead", {
|
||||
gardenOrgId: "697986",
|
||||
plantedDate: "2021-09-21",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(8))),
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(2.2)), geo.south(geo.ft(8.2))),
|
||||
new Flower("Tiny Tortuga Turtlehead", {
|
||||
gardenOrgId: "697986",
|
||||
plantedDate: "2021-09-21",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(17))),
|
||||
new Flower("Peony", {
|
||||
gardenOrgId: "697986",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(19)), geo.south(geo.ft(1.5))),
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(18.7)), geo.south(geo.ft(1.6))),
|
||||
new Flower("Peony", {
|
||||
gardenOrgId: "697986",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(75)), geo.south(geo.ft(2))),
|
||||
@@ -142,23 +202,19 @@ const houseplants = [
|
||||
|
||||
const cutDownTrees = [
|
||||
new Tree("Maybe elm", {
|
||||
canopyDiameterMeters: 15,
|
||||
heightMeters: 15,
|
||||
measurements: [{ canopyDiameter: geo.ft(49.21), height: geo.ft(49.21) }],
|
||||
removedDate: "2021-05-05",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(65)), geo.south(geo.ft(-2))),
|
||||
new Tree("Big pine tree", {
|
||||
canopyDiameterMeters: 8,
|
||||
heightMeters: 15,
|
||||
measurements: [{ canopyDiameter: geo.ft(26.25), height: geo.ft(49.21) }],
|
||||
removedDate: "2021-05-06",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(98)), geo.south(geo.ft(-15))),
|
||||
new Tree("Walnut by house", {
|
||||
canopyDiameterMeters: 6,
|
||||
heightMeters: 7,
|
||||
measurements: [{ canopyDiameter: geo.ft(19.69), height: geo.ft(22.97) }],
|
||||
removedDate: "2021-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(32)), geo.south(geo.ft(40))),
|
||||
new Tree("Walnut by shed", {
|
||||
canopyDiameterMeters: 7,
|
||||
heightMeters: 7,
|
||||
measurements: [{ canopyDiameter: geo.ft(22.97), height: geo.ft(22.97) }],
|
||||
removedDate: "2021-05-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(100)), geo.south(geo.ft(40))),
|
||||
];
|
||||
@@ -166,121 +222,152 @@ const cutDownTrees = [
|
||||
const trees = [
|
||||
new Tree("Northern Empress Japanese Elm", {
|
||||
species: "Northern Empress Japanese Elm",
|
||||
canopyDiameterMeters: 6,
|
||||
heightMeters: 7,
|
||||
measurements: [{ canopyDiameter: geo.ft(19.69), height: geo.ft(22.97) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 312,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(9)), geo.south(geo.ft(16))),
|
||||
new Tree("Hot Wings Maple", {
|
||||
species: "Hot Wings Maple",
|
||||
canopyDiameterMeters: 5,
|
||||
heightMeters: 5,
|
||||
measurements: [{ canopyDiameter: geo.ft(16.4), height: geo.ft(16.4) }],
|
||||
gardenOrgId: "536120",
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 186,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(60)), geo.south(geo.ft(40))),
|
||||
new Tree("Prairie Expedition American Elm", {
|
||||
species: "Prairie Expedition American Elm",
|
||||
canopyDiameterMeters: 5,
|
||||
heightMeters: 5,
|
||||
measurements: [{ canopyDiameter: geo.ft(16.4), height: geo.ft(16.4) }],
|
||||
gardenOrgId: "736267",
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 280,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(95)), geo.south(geo.ft(15))),
|
||||
|
||||
new Tree("Dwarf Korean Lilac", {
|
||||
species: "Dwarf Korean Lilac",
|
||||
canopyDiameterMeters: 1.75,
|
||||
heightMeters: 2.5,
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(5.5), height: geo.ft(6.7) },
|
||||
],
|
||||
gardenOrgId: "79137",
|
||||
plantedDate: "2021-09-21",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(3))),
|
||||
pricePaid: 218,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3.5)), geo.south(geo.ft(4.8))),
|
||||
|
||||
new Tree("Dwarf Korean Lilac", {
|
||||
species: "Dwarf Korean Lilac",
|
||||
canopyDiameterMeters: 1.75,
|
||||
heightMeters: 2.5,
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(5.0), height: geo.ft(6.8) },
|
||||
],
|
||||
gardenOrgId: "79137",
|
||||
plantedDate: "2021-09-21",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(20))),
|
||||
pricePaid: 218,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3.7)), geo.south(geo.ft(21.4))),
|
||||
|
||||
new Tree("Dwarf Korean Lilac", {
|
||||
species: "Dwarf Korean Lilac",
|
||||
canopyDiameterMeters: 1.75,
|
||||
heightMeters: 2.5,
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(5.5), height: geo.ft(7.5) },
|
||||
],
|
||||
gardenOrgId: "79137",
|
||||
plantedDate: "2021-09-21",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(37))),
|
||||
pricePaid: 218,
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(3.8)), geo.south(geo.ft(33.1))),
|
||||
|
||||
new Tree("Boulevard Linden", {
|
||||
species: "Boulevard Linden",
|
||||
canopyDiameterMeters: 3,
|
||||
heightMeters: 6,
|
||||
measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(19.69) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 327,
|
||||
}).add(geo.lngLat(-100.8990003556445, 46.826776648309774)),
|
||||
new Tree("Boulevard Linden 2", {
|
||||
species: "Boulevard Linden",
|
||||
canopyDiameterMeters: 3,
|
||||
heightMeters: 6,
|
||||
measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(19.69) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 327,
|
||||
}).add(geo.lngLat(-100.89898269567485, 46.826709328342595)),
|
||||
new Tree("Evergreen", {
|
||||
species: "Evergreen",
|
||||
canopyDiameterMeters: 8,
|
||||
heightMeters: 15,
|
||||
measurements: [{ canopyDiameter: geo.ft(26.25), height: geo.ft(49.21) }],
|
||||
}).add(geo.lngLat(-100.89888268258135, 46.826680107011924)),
|
||||
|
||||
new Tree("Black Walnut from Jean", {
|
||||
species: "Black Walnut",
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(2.7), height: geo.ft(2.9) }
|
||||
],
|
||||
plantedDate: "2024-10-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(2.8)), geo.south(geo.ft(9.2))),
|
||||
|
||||
new Tree("Black Walnut from Jean", {
|
||||
species: "Black Walnut",
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(2.5), height: geo.ft(3.5) }
|
||||
],
|
||||
plantedDate: "2024-10-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(2.6)), geo.south(geo.ft(11.3))),
|
||||
|
||||
new Tree("Black Walnut from Jean", {
|
||||
species: "Black Walnut",
|
||||
measurements: [
|
||||
{ measuredDate: "2026-07-08", canopyDiameter: geo.ft(4), height: geo.ft(4.5) }
|
||||
],
|
||||
plantedDate: "2024-10-01",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(1.6)), geo.south(geo.ft(12.7))),
|
||||
];
|
||||
|
||||
const bushes = [
|
||||
new Tree("Medora Juniper 1", {
|
||||
new Bush("Medora Juniper 1", {
|
||||
species: "Medora Juniper",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 2.5,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 96.50,
|
||||
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89880933964611, 46.826705259550664)),
|
||||
new Tree("Medora Juniper 2", {
|
||||
new Bush("Medora Juniper 2", {
|
||||
species: "Medora Juniper",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 2.5,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 96.50,
|
||||
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89877004677517, 46.82670396593881)),
|
||||
new Tree("Medora Juniper 3", {
|
||||
new Bush("Medora Juniper 3", {
|
||||
species: "Medora Juniper",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 2.5,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 96.50,
|
||||
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.898799106227, 46.82669830398086)),
|
||||
new Tree("Medora Juniper 4", {
|
||||
new Bush("Medora Juniper 4", {
|
||||
species: "Medora Juniper",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 2.5,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 96.50,
|
||||
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89878871410515, 46.82669251034885)),
|
||||
new Tree("Medora Juniper 5", {
|
||||
new Bush("Medora Juniper 5", {
|
||||
species: "Medora Juniper",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 2.5,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
|
||||
plantedDate: "2021-09-21",
|
||||
pricePaid: 96.50,
|
||||
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.8987812086838, 46.82671120797719)),
|
||||
new Tree("Black Currant", {
|
||||
new Bush("Black Currant", {
|
||||
species: "Black Currant",
|
||||
canopyDiameterMeters: 2,
|
||||
heightMeters: 2,
|
||||
measurements: [{ canopyDiameter: geo.ft(6.56), height: geo.ft(6.56) }],
|
||||
gardenOrgId: "87767",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(107)), geo.south(geo.ft(3))),
|
||||
new Tree("Elderberry", {
|
||||
new Bush("Elderberry", {
|
||||
species: "Elderberry",
|
||||
canopyDiameterMeters: 3,
|
||||
heightMeters: 3,
|
||||
measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(9.84) }],
|
||||
gardenOrgId: "78882",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(142)), geo.south(geo.ft(52))),
|
||||
new Tree("Spirea", {
|
||||
new Bush("Spirea", {
|
||||
species: "Spirea",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 1,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
|
||||
gardenOrgId: "79059",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(70)), geo.south(geo.ft(31))),
|
||||
new Tree("Spirea", {
|
||||
new Bush("Spirea", {
|
||||
species: "Spirea",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 1,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
|
||||
gardenOrgId: "79059",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(74)), geo.south(geo.ft(31))),
|
||||
new Tree("Spirea", {
|
||||
new Bush("Spirea", {
|
||||
species: "Spirea",
|
||||
canopyDiameterMeters: 1.5,
|
||||
heightMeters: 1,
|
||||
measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
|
||||
gardenOrgId: "79068",
|
||||
}).add(geo.offset(lotCorner), geo.east(geo.ft(78)), geo.south(geo.ft(31))),
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"nodes": [
|
||||
"visibility:category:plants",
|
||||
"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:87",
|
||||
"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:90",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:feature:91",
|
||||
"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:57",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:58",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:59",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:60",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:61",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:62",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:63",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:64",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:65",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:66",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:67",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:68",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:69",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:70",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:71",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:72",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:73",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:74",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:75",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:76",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:77",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:78",
|
||||
"visibility:category:plants>visibility:type:flower>visibility:type:daylily>visibility:feature:79",
|
||||
"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:82",
|
||||
"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:96",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:97",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:98",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:99",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:100",
|
||||
"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:103",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:114",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:115",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:116",
|
||||
"visibility:category:plants>visibility:type:tree>visibility:feature:117",
|
||||
"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:113",
|
||||
"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:108",
|
||||
"visibility:category:structures",
|
||||
"visibility:category:structures>visibility:type:fence",
|
||||
"visibility:category:structures>visibility:type:fence>visibility:feature:1",
|
||||
"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:52",
|
||||
"visibility:category:structures>visibility:group:buildings",
|
||||
"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:group:house-parts",
|
||||
"visibility:category:structures>visibility:group:buildings>visibility:group:house-parts>visibility:feature:42",
|
||||
"visibility:category:structures>visibility:group:buildings>visibility:group:house-parts>visibility:feature:43",
|
||||
"visibility:category:structures>visibility:group:paved-surfaces",
|
||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:44",
|
||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:45",
|
||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:46",
|
||||
"visibility:category:structures>visibility:group:paved-surfaces>visibility:feature:47",
|
||||
"visibility:category:infrastructure",
|
||||
"visibility:category:infrastructure>visibility:type:sprinkler",
|
||||
"visibility:category:infrastructure>visibility:type:sprinkler>visibility:feature:118",
|
||||
"visibility:category:infrastructure>visibility:type:sprinkler>visibility:feature:119",
|
||||
"visibility:category:reference",
|
||||
"visibility:category:reference>visibility:type:lotBoundary",
|
||||
"visibility:category:reference>visibility:type:lotBoundary>visibility:feature:0",
|
||||
"visibility:category:reference>visibility:group:yard-grid",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:2",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:3",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:4",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:5",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:6",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:7",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:8",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:9",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:10",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:11",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:12",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:13",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:14",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:15",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:16",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:17",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:18",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:19",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:20",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:21",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:22",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:23",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:24",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:25",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:26",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:27",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:28",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:29",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:30",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:31",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:32",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:33",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:34",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:35",
|
||||
"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:38",
|
||||
"visibility:category:reference>visibility:group:yard-grid>visibility:feature:39"
|
||||
]
|
||||
}
|
||||
+2
-1
@@ -3,7 +3,7 @@ import { boundaryFeatures } from "./boundaries.js";
|
||||
import { buildingFeatures, buildingGroups } from "./buildings.js";
|
||||
import { gridFeatures, gridGroups } from "./grid.js";
|
||||
import { irrigationFeatures } from "./irrigation.js";
|
||||
import { pavementFeatures } from "./pavement.js";
|
||||
import { pavementFeatures, pavementGroups } from "./pavement.js";
|
||||
import { plantFeatures, plantGroups } from "./plants.js";
|
||||
|
||||
export const yardGeoJSON = collectYardData(
|
||||
@@ -18,6 +18,7 @@ export const yardGeoJSON = collectYardData(
|
||||
[
|
||||
...buildingGroups,
|
||||
...gridGroups,
|
||||
...pavementGroups,
|
||||
...plantGroups,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
export const featureCategories = {
|
||||
plants: { label: "Plants", order: 10 },
|
||||
structures: { label: "Structures", order: 20 },
|
||||
infrastructure: { label: "Infrastructure", order: 30 },
|
||||
reference: { label: "Map Reference", order: 40 },
|
||||
};
|
||||
|
||||
export const featureTypes = {
|
||||
lotBoundary: {
|
||||
label: "Lot Boundary",
|
||||
categoryId: "reference",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -13,6 +21,7 @@ export const featureTypes = {
|
||||
},
|
||||
fence: {
|
||||
label: "Fence",
|
||||
categoryId: "structures",
|
||||
geometryType: "LineString",
|
||||
detailFields: {
|
||||
fenceType: "string",
|
||||
@@ -25,6 +34,7 @@ export const featureTypes = {
|
||||
},
|
||||
yardGrid: {
|
||||
label: "Yard Grid",
|
||||
categoryId: "reference",
|
||||
geometryType: "LineString",
|
||||
detailFields: {
|
||||
axis: "string",
|
||||
@@ -40,6 +50,8 @@ export const featureTypes = {
|
||||
},
|
||||
house: {
|
||||
label: "House",
|
||||
collectionLabel: "Houses",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -51,6 +63,8 @@ export const featureTypes = {
|
||||
},
|
||||
garage: {
|
||||
label: "Garage",
|
||||
collectionLabel: "Garages",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -62,6 +76,7 @@ export const featureTypes = {
|
||||
},
|
||||
porch: {
|
||||
label: "Porch",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -73,6 +88,7 @@ export const featureTypes = {
|
||||
},
|
||||
deck: {
|
||||
label: "Deck",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -84,6 +100,8 @@ export const featureTypes = {
|
||||
},
|
||||
walkway: {
|
||||
label: "Walkway",
|
||||
collectionLabel: "Walkways",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -95,6 +113,8 @@ export const featureTypes = {
|
||||
},
|
||||
driveway: {
|
||||
label: "Driveway",
|
||||
collectionLabel: "Driveways",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {},
|
||||
style: {
|
||||
@@ -106,6 +126,8 @@ export const featureTypes = {
|
||||
},
|
||||
flowerBed: {
|
||||
label: "Flower Bed",
|
||||
collectionLabel: "Flower Beds",
|
||||
categoryId: "structures",
|
||||
geometryType: "Polygon",
|
||||
detailFields: {
|
||||
soil: "string",
|
||||
@@ -121,6 +143,7 @@ export const featureTypes = {
|
||||
},
|
||||
plant: {
|
||||
label: "Plant",
|
||||
categoryId: "plants",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
commonName: "string",
|
||||
@@ -128,6 +151,9 @@ export const featureTypes = {
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
gardenOrgId: "string",
|
||||
measurements: "plantMeasurement[]",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
note: "string",
|
||||
},
|
||||
externalLinks: [
|
||||
@@ -149,6 +175,7 @@ export const featureTypes = {
|
||||
},
|
||||
flower: {
|
||||
label: "Flower",
|
||||
collectionLabel: "Flowers",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
@@ -158,6 +185,9 @@ export const featureTypes = {
|
||||
cultivar: "string",
|
||||
bloomColor: "string",
|
||||
bloomSeason: "string",
|
||||
measurements: "plantMeasurement[]",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: {
|
||||
@@ -171,6 +201,7 @@ export const featureTypes = {
|
||||
},
|
||||
daylily: {
|
||||
label: "Daylily",
|
||||
collectionLabel: "Daylilies",
|
||||
parentKind: "flower",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
@@ -179,6 +210,9 @@ export const featureTypes = {
|
||||
cultivar: "string",
|
||||
bloomColor: "string",
|
||||
bloomSeason: "string",
|
||||
measurements: "plantMeasurement[]",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: {
|
||||
@@ -193,6 +227,7 @@ export const featureTypes = {
|
||||
},
|
||||
shrub: {
|
||||
label: "Shrub",
|
||||
collectionLabel: "Shrubs",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
@@ -200,20 +235,22 @@ export const featureTypes = {
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
canopyDiameterMeters: "number",
|
||||
heightMeters: "number",
|
||||
measurements: "plantMeasurement[]",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: (details) => ({
|
||||
color: "#426a3d",
|
||||
weight: 1,
|
||||
radiusMeters: (details.canopyDiameterMeters ?? 1) / 2,
|
||||
radiusMeters: (details.canopyDiameter ?? 1) / 2,
|
||||
fillColor: "#7e9d58",
|
||||
fillOpacity: 0.62,
|
||||
}),
|
||||
},
|
||||
tree: {
|
||||
label: "Tree",
|
||||
collectionLabel: "Trees",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
@@ -221,20 +258,22 @@ export const featureTypes = {
|
||||
genus: "string",
|
||||
species: "string",
|
||||
cultivar: "string",
|
||||
canopyDiameterMeters: "number",
|
||||
heightMeters: "number",
|
||||
measurements: "plantMeasurement[]",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
note: "string",
|
||||
},
|
||||
style: (details) => ({
|
||||
color: "#2f6b3d",
|
||||
weight: 1,
|
||||
radiusMeters: (details.canopyDiameterMeters ?? 2) / 2,
|
||||
radiusMeters: (details.canopyDiameter ?? 2) / 2,
|
||||
fillColor: "#5c9e64",
|
||||
fillOpacity: 0.55,
|
||||
}),
|
||||
},
|
||||
sprinkler: {
|
||||
label: "Sprinkler",
|
||||
categoryId: "infrastructure",
|
||||
geometryType: "Point",
|
||||
detailFields: {
|
||||
zone: "string",
|
||||
@@ -248,6 +287,20 @@ export const featureTypes = {
|
||||
fillOpacity: 0.95,
|
||||
},
|
||||
},
|
||||
bush: {
|
||||
label: "Bush",
|
||||
collectionLabel: "Bushes",
|
||||
parentKind: "plant",
|
||||
geometryType: "Point",
|
||||
detailFields: {},
|
||||
style: (details) => ({
|
||||
color: "#426a3d",
|
||||
weight: 1,
|
||||
radiusMeters: (details.canopyDiameter ?? 1) / 2,
|
||||
fillColor: "#7e9d58",
|
||||
fillOpacity: 0.62,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export function getFeatureType(kind) {
|
||||
@@ -263,6 +316,8 @@ export function listFeatureTypeSchemas() {
|
||||
return Object.entries(featureTypes).map(([kind, definition]) => ({
|
||||
kind,
|
||||
label: definition.label,
|
||||
collectionLabel: definition.collectionLabel ?? definition.label,
|
||||
categoryId: getFeatureCategoryId(kind),
|
||||
parentKind: definition.parentKind ?? null,
|
||||
geometryType: definition.geometryType,
|
||||
detailFields: getInheritedDetailFields(kind),
|
||||
@@ -272,6 +327,12 @@ export function listFeatureTypeSchemas() {
|
||||
}));
|
||||
}
|
||||
|
||||
export function listFeatureCategorySchemas() {
|
||||
return Object.entries(featureCategories)
|
||||
.map(([id, definition]) => ({ id, ...definition }))
|
||||
.sort((left, right) => left.order - right.order);
|
||||
}
|
||||
|
||||
export function resolveFeatureStyle(kind, details = {}) {
|
||||
const { style } = getFeatureType(kind);
|
||||
return typeof style === "function" ? style(details) : { ...style };
|
||||
@@ -299,6 +360,14 @@ function getFeatureTypeLineage(kind) {
|
||||
return lineage;
|
||||
}
|
||||
|
||||
function getFeatureCategoryId(kind) {
|
||||
const definition = [...getFeatureTypeLineage(kind)].reverse().find((entry) => entry.categoryId);
|
||||
if (!definition || !featureCategories[definition.categoryId]) {
|
||||
throw new Error(`Feature type has an unknown or missing category: ${kind}`);
|
||||
}
|
||||
return definition.categoryId;
|
||||
}
|
||||
|
||||
function getInheritedDetailFields(kind) {
|
||||
return Object.assign(
|
||||
{},
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
|
||||
import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js";
|
||||
import {
|
||||
getFeatureType,
|
||||
listFeatureCategorySchemas,
|
||||
listFeatureTypeSchemas,
|
||||
} from "./feature-types.js";
|
||||
|
||||
export const YARD_SCHEMA_VERSION = 2;
|
||||
export const YARD_SCHEMA_VERSION = 4;
|
||||
|
||||
class YardFeature {
|
||||
constructor(kind, geometryType, name, details = {}) {
|
||||
@@ -155,6 +159,8 @@ export function defineGroup(id, label, options = {}) {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
categoryId: options.categoryId ?? null,
|
||||
parentKind: options.parentKind ?? null,
|
||||
parentGroupId: options.parentGroupId ?? null,
|
||||
};
|
||||
}
|
||||
@@ -286,7 +292,7 @@ export class FlowerBed extends PolygonFeature {
|
||||
|
||||
export class Plant extends PointFeature {
|
||||
constructor(name = "Plant", details = {}, kind = "plant") {
|
||||
super(kind, name, details);
|
||||
super(kind, name, normalizePlantDetails(details));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +313,14 @@ export class Daylily extends Flower {
|
||||
}
|
||||
|
||||
export class Shrub extends Plant {
|
||||
constructor(name = "Shrub", details = {}) {
|
||||
super(name, details, "shrub");
|
||||
constructor(name = "Shrub", details = {}, kind = "shrub") {
|
||||
super(name, details, kind);
|
||||
}
|
||||
}
|
||||
|
||||
export class Bush extends Shrub {
|
||||
constructor(name = "Bush", details = {}) {
|
||||
super(name, details);
|
||||
super(name, details, "bush");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +330,65 @@ export class Tree extends Plant {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePlantDetails(details) {
|
||||
const {
|
||||
canopyDiameter,
|
||||
height,
|
||||
measurements = [],
|
||||
...restDetails
|
||||
} = details;
|
||||
const normalizedMeasurements = normalizePlantMeasurements(measurements);
|
||||
|
||||
if (!normalizedMeasurements.length && hasPlantSizeMeasurement(details)) {
|
||||
normalizedMeasurements.push(
|
||||
normalizePlantMeasurement({
|
||||
canopyDiameter,
|
||||
height,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const latestMeasurement = normalizedMeasurements.at(-1);
|
||||
|
||||
return {
|
||||
...restDetails,
|
||||
...(normalizedMeasurements.length ? { measurements: normalizedMeasurements } : {}),
|
||||
...(latestMeasurement?.canopyDiameter !== undefined
|
||||
? { canopyDiameter: latestMeasurement.canopyDiameter }
|
||||
: {}),
|
||||
...(latestMeasurement?.height !== undefined
|
||||
? { height: latestMeasurement.height }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlantMeasurements(measurements) {
|
||||
if (!Array.isArray(measurements)) {
|
||||
throw new Error("Plant measurements must be an array");
|
||||
}
|
||||
|
||||
return measurements.map(normalizePlantMeasurement);
|
||||
}
|
||||
|
||||
function normalizePlantMeasurement(measurement) {
|
||||
if (!measurement || typeof measurement !== "object") {
|
||||
throw new Error("Plant measurement entries must be objects");
|
||||
}
|
||||
|
||||
const { canopyDiameter, height, measuredDate, ...restMeasurement } = measurement;
|
||||
|
||||
return {
|
||||
...restMeasurement,
|
||||
...(measuredDate ? { measuredDate } : {}),
|
||||
...(canopyDiameter !== undefined ? { canopyDiameter } : {}),
|
||||
...(height !== undefined ? { height } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function hasPlantSizeMeasurement(details) {
|
||||
return details.canopyDiameter !== undefined || details.height !== undefined;
|
||||
}
|
||||
|
||||
export class Sprinkler extends PointFeature {
|
||||
constructor(name = "Sprinkler", details = {}) {
|
||||
super("sprinkler", name, details);
|
||||
@@ -337,10 +402,18 @@ export function collectYardData(features, groups = []) {
|
||||
plantedDate: "date",
|
||||
removedDate: "date",
|
||||
};
|
||||
collection.measurementFields = {
|
||||
measuredDate: "date",
|
||||
canopyDiameter: "number",
|
||||
height: "number",
|
||||
};
|
||||
collection.featureTypes = listFeatureTypeSchemas();
|
||||
collection.featureCategories = listFeatureCategorySchemas();
|
||||
collection.groups = groups.map((group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
categoryId: group.categoryId,
|
||||
parentKind: group.parentKind,
|
||||
parentGroupId: group.parentGroupId ?? null,
|
||||
}));
|
||||
return collection;
|
||||
|
||||
+284
-40
@@ -5,9 +5,11 @@ import {
|
||||
addProtocol,
|
||||
} from "../vendor/maplibre/maplibre-gl.mjs";
|
||||
import { yardGeoJSON } from "./data/yard.js";
|
||||
import { CheckboxTree, loadManifest } from "../vendor/checkbox-tree/checkbox-tree.js";
|
||||
import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";
|
||||
|
||||
const MAP_MAX_ZOOM = 24;
|
||||
const FEET_PER_METER = 3.280839895;
|
||||
const MANDAN_BOUNDS = [
|
||||
[-101.05, 46.74],
|
||||
[-100.75, 46.93],
|
||||
@@ -20,7 +22,6 @@ const TIMELINE_PRESETS = [
|
||||
|
||||
const mapElement = document.getElementById("map");
|
||||
const statusElement = document.getElementById("map-status");
|
||||
const legend = document.getElementById("legend");
|
||||
const protocol = new globalThis.pmtiles.Protocol();
|
||||
addProtocol("pmtiles", protocol.tile);
|
||||
const basemapLayers = globalThis.basemaps
|
||||
@@ -50,11 +51,14 @@ const map = new MapLibreMap({
|
||||
map.addControl(new NavigationControl(), "bottom-right");
|
||||
|
||||
let selectedTimelineDate = TIMELINE_PRESETS.at(-1).date;
|
||||
let selectedFeatureIndexes = new Set(yardGeoJSON.features.map((_, index) => index));
|
||||
let renderedLayerIds = [];
|
||||
let renderedSourceIds = [];
|
||||
let renderedLayerHandlers = [];
|
||||
const visibilityReady = initializeVisibilityTree();
|
||||
|
||||
map.on("load", () => {
|
||||
map.on("load", async () => {
|
||||
await visibilityReady;
|
||||
renderYardForDate(selectedTimelineDate);
|
||||
addTimelineControl();
|
||||
map.fitBounds(getGeoJSONBounds(yardGeoJSON), {
|
||||
@@ -78,10 +82,12 @@ map.on("error", (event) => {
|
||||
|
||||
function renderYardForDate(date) {
|
||||
removeRenderedYard();
|
||||
const features = yardGeoJSON.features.filter((feature) => featureExistsOnDate(feature, date));
|
||||
const features = yardGeoJSON.features.filter(
|
||||
(feature, index) =>
|
||||
selectedFeatureIndexes.has(index) && featureExistsOnDate(feature, date),
|
||||
);
|
||||
|
||||
features.forEach((feature, index) => addFeatureLayer(feature, index));
|
||||
renderLegend(features);
|
||||
syncTimelineButtons();
|
||||
}
|
||||
|
||||
@@ -207,6 +213,199 @@ function getCircleRadius(style, latitude) {
|
||||
];
|
||||
}
|
||||
|
||||
async function initializeVisibilityTree() {
|
||||
const container = document.getElementById("visibility-tree");
|
||||
const manifest = loadManifest(
|
||||
await fetch("./src/data/visibility-tree-manifest.json").then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Visibility manifest returned HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}),
|
||||
);
|
||||
const tree = new CheckboxTree(container, {
|
||||
initiallyCollapsed: true,
|
||||
initiallySelected: true,
|
||||
storageKey: "a-plot-of-plants:visibility:v1",
|
||||
manifest,
|
||||
stateEncoding: "tiered",
|
||||
onSelectionChange: ({ selectedNodes }) => {
|
||||
selectedFeatureIndexes = getSelectedFeatureIndexes(selectedNodes);
|
||||
if (map.loaded()) {
|
||||
renderYardForDate(selectedTimelineDate);
|
||||
}
|
||||
pushTreeState(tree);
|
||||
},
|
||||
});
|
||||
|
||||
tree.setData(buildVisibilityNodes(yardGeoJSON));
|
||||
selectedFeatureIndexes = getSelectedFeatureIndexes(tree.getSelectedNodes());
|
||||
|
||||
container.addEventListener("click", (event) => {
|
||||
if (event.target.closest(".checkbox-tree__toggle")) {
|
||||
pushTreeState(tree);
|
||||
}
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
tree.restoreStateFromLocation();
|
||||
selectedFeatureIndexes = getSelectedFeatureIndexes(tree.getSelectedNodes());
|
||||
if (map.loaded()) {
|
||||
renderYardForDate(selectedTimelineDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pushTreeState(tree) {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = tree.serializeStateToFragment();
|
||||
if (url.href !== window.location.href) {
|
||||
history.pushState(null, "", url);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedFeatureIndexes(nodes) {
|
||||
return new Set(
|
||||
nodes
|
||||
.map((node) => node.metadata?.featureIndex)
|
||||
.filter((index) => Number.isInteger(index)),
|
||||
);
|
||||
}
|
||||
|
||||
function buildVisibilityNodes(collection) {
|
||||
const typeSchemasByKind = new globalThis.Map(
|
||||
collection.featureTypes.map((type) => [type.kind, type]),
|
||||
);
|
||||
const categoriesById = new globalThis.Map(
|
||||
collection.featureCategories.map((category) => [
|
||||
category.id,
|
||||
{
|
||||
id: `visibility:category:${category.id}`,
|
||||
label: category.label,
|
||||
order: category.order,
|
||||
children: [],
|
||||
},
|
||||
]),
|
||||
);
|
||||
const featureEntries = collection.features.map((feature, featureIndex) => ({
|
||||
feature,
|
||||
featureIndex,
|
||||
}));
|
||||
const groupsById = new globalThis.Map(
|
||||
collection.groups.map((group) => [
|
||||
group.id,
|
||||
{
|
||||
id: `visibility:${group.id}`,
|
||||
label: group.label,
|
||||
children: [],
|
||||
parentGroupId: group.parentGroupId,
|
||||
parentKind: group.parentKind,
|
||||
categoryId: group.categoryId,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const groupedFeatureIndexes = new Set();
|
||||
|
||||
for (const entry of featureEntries) {
|
||||
const group = entry.feature.properties.groupIds.map((id) => groupsById.get(id)).find(Boolean);
|
||||
if (!group) continue;
|
||||
const categoryId = typeSchemasByKind.get(entry.feature.properties.kind)?.categoryId;
|
||||
if (group.categoryId && group.categoryId !== categoryId) {
|
||||
throw new Error(`Visibility group spans multiple feature categories: ${group.label}`);
|
||||
}
|
||||
group.categoryId = categoryId;
|
||||
group.children.push(toVisibilityLeaf(entry));
|
||||
groupedFeatureIndexes.add(entry.featureIndex);
|
||||
}
|
||||
|
||||
for (const group of groupsById.values()) {
|
||||
const parent = groupsById.get(group.parentGroupId);
|
||||
if (parent) {
|
||||
if (parent.categoryId !== group.categoryId) {
|
||||
throw new Error(`Nested visibility groups must share a category: ${group.label}`);
|
||||
}
|
||||
parent.children.push(group);
|
||||
}
|
||||
}
|
||||
const rootGroups = [...groupsById.values()].filter(
|
||||
(group) => !groupsById.has(group.parentGroupId) && group.children.length > 0,
|
||||
);
|
||||
|
||||
const featuresByKind = new globalThis.Map();
|
||||
for (const entry of featureEntries) {
|
||||
if (groupedFeatureIndexes.has(entry.featureIndex)) continue;
|
||||
const { kind } = entry.feature.properties;
|
||||
const typeSchema = typeSchemasByKind.get(kind);
|
||||
const categoryId = typeSchema?.categoryId;
|
||||
const typeNode = featuresByKind.get(kind) ?? {
|
||||
id: `visibility:type:${kind}`,
|
||||
label: typeSchema?.collectionLabel ?? entry.feature.properties.label,
|
||||
kind,
|
||||
parentKind: typeSchema?.parentKind,
|
||||
categoryId,
|
||||
children: [],
|
||||
};
|
||||
typeNode.children.push(toVisibilityLeaf(entry));
|
||||
featuresByKind.set(kind, typeNode);
|
||||
}
|
||||
|
||||
for (const typeNode of featuresByKind.values()) {
|
||||
const parentTypeNode = featuresByKind.get(typeNode.parentKind);
|
||||
if (parentTypeNode) {
|
||||
parentTypeNode.children.push(typeNode);
|
||||
} else {
|
||||
categoriesById.get(typeNode.categoryId)?.children.push(typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of rootGroups) {
|
||||
const parentTypeNode = featuresByKind.get(group.parentKind);
|
||||
if (parentTypeNode) {
|
||||
parentTypeNode.children.push(group);
|
||||
} else {
|
||||
categoriesById.get(group.categoryId)?.children.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
for (const typeNode of featuresByKind.values()) {
|
||||
delete typeNode.kind;
|
||||
delete typeNode.parentKind;
|
||||
delete typeNode.categoryId;
|
||||
}
|
||||
for (const group of groupsById.values()) {
|
||||
delete group.parentGroupId;
|
||||
delete group.parentKind;
|
||||
delete group.categoryId;
|
||||
}
|
||||
|
||||
return [...categoriesById.values()]
|
||||
.filter((category) => category.children.length > 0)
|
||||
.sort((left, right) => left.order - right.order)
|
||||
.map((category) => {
|
||||
delete category.order;
|
||||
return category;
|
||||
})
|
||||
.map(sortVisibilityNode);
|
||||
}
|
||||
|
||||
function toVisibilityLeaf({ feature, featureIndex }) {
|
||||
return {
|
||||
id: `visibility:feature:${featureIndex}`,
|
||||
label: feature.properties.name,
|
||||
metadata: { featureIndex },
|
||||
};
|
||||
}
|
||||
|
||||
function sortVisibilityNode(node) {
|
||||
if (node.children) {
|
||||
node.children = node.children.map(sortVisibilityNode).sort(compareVisibilityNodes);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function compareVisibilityNodes(left, right) {
|
||||
return left.label.localeCompare(right.label, undefined, { numeric: true });
|
||||
}
|
||||
|
||||
function featureExistsOnDate(feature, date) {
|
||||
const { plantedDate, removedDate } = feature.properties;
|
||||
return (!plantedDate || plantedDate <= date) && (!removedDate || date <= removedDate);
|
||||
@@ -263,36 +462,6 @@ function syncTimelineButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderLegend(features) {
|
||||
const summary = new globalThis.Map();
|
||||
|
||||
for (const feature of features) {
|
||||
const key = feature.properties.kind;
|
||||
const style = resolveFeatureStyle(key, feature.properties.details);
|
||||
const existing = summary.get(key) ?? {
|
||||
label: feature.properties.label,
|
||||
count: 0,
|
||||
color: style.fillColor ?? style.color ?? "#333333",
|
||||
};
|
||||
existing.count += 1;
|
||||
summary.set(key, existing);
|
||||
}
|
||||
|
||||
legend.replaceChildren(
|
||||
...[...summary.values()].map((entry) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "legend-item";
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "legend-swatch";
|
||||
swatch.style.background = entry.color;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = `${entry.label} (${entry.count})`;
|
||||
row.append(swatch, label);
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getTodayDateString(date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -331,7 +500,8 @@ function visitCoordinates(coordinates, callback) {
|
||||
function buildPopupContent(properties) {
|
||||
const { kind, label, name, plantedDate, removedDate, details, parentId, groupIds } = properties;
|
||||
const entries = Object.entries(details ?? {}).filter(
|
||||
([key, value]) => value !== null && value !== undefined && shouldShowDetailField(key),
|
||||
([key, value]) =>
|
||||
value !== null && value !== undefined && shouldShowDetailField(key, details),
|
||||
);
|
||||
const lifecycleEntries = [];
|
||||
const relationshipEntries = [];
|
||||
@@ -345,6 +515,11 @@ function buildPopupContent(properties) {
|
||||
const title = `<strong>${escapeHtml(name)}</strong>`;
|
||||
const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
|
||||
const allEntries = [...lifecycleEntries, ...entries, ...relationshipEntries];
|
||||
const measurementTable = buildMeasurementTable(details?.measurements);
|
||||
if (allEntries.length === 0 && linkEntries.length === 0 && !measurementTable) {
|
||||
return `${title}${subtitle}`;
|
||||
}
|
||||
|
||||
const detailRows = allEntries
|
||||
.map(
|
||||
([key, value]) =>
|
||||
@@ -357,17 +532,28 @@ function buildPopupContent(properties) {
|
||||
`<div><a href="${escapeHtml(link.url)}" target="_blank" rel="noreferrer">${escapeHtml(link.label)}</a></div>`,
|
||||
)
|
||||
.join("");
|
||||
return `${title}${subtitle}<div>${detailRows}${linkRows}</div>`;
|
||||
return `${title}${subtitle}<div>${detailRows}${measurementTable}${linkRows}</div>`;
|
||||
}
|
||||
|
||||
function shouldShowDetailField(key) {
|
||||
function shouldShowDetailField(key, details = {}) {
|
||||
if (key === "measurements") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hasDatedMeasurements(details.measurements) &&
|
||||
(key === "canopyDiameter" || key === "height")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !key.endsWith("Url") && key !== "gardenOrgId";
|
||||
}
|
||||
|
||||
function formatDetailLabel(key) {
|
||||
const labels = {
|
||||
canopyDiameterMeters: "Canopy diameter",
|
||||
heightMeters: "Height",
|
||||
canopyDiameter: "Canopy diameter",
|
||||
height: "Height",
|
||||
measurements: "Measurements",
|
||||
measuredDate: "Measured",
|
||||
plantedDate: "Planted",
|
||||
removedDate: "Removed",
|
||||
commonName: "Common name",
|
||||
@@ -388,11 +574,69 @@ function formatDetailLabel(key) {
|
||||
}
|
||||
|
||||
function formatDetailValue(key, value) {
|
||||
if (key === "canopyDiameterMeters" || key === "heightMeters") return `${value} m`;
|
||||
if (key === "canopyDiameter" || key === "height") return formatMetersAsFeet(value);
|
||||
if (key === "offsetFeet" || key === "spacingFeet") return `${value} ft`;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatMetersAsFeet(value) {
|
||||
return `${formatNumber(value * FEET_PER_METER)} ft`;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return Number(value.toFixed(1)).toString();
|
||||
}
|
||||
|
||||
function buildMeasurementTable(measurements) {
|
||||
const datedMeasurements = getDatedMeasurements(measurements);
|
||||
if (!datedMeasurements.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const rows = datedMeasurements
|
||||
.map(
|
||||
(measurement) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(measurement.measuredDate)}</td>
|
||||
<td>${escapeHtml(formatOptionalMeasurement(measurement.canopyDiameter))}</td>
|
||||
<td>${escapeHtml(formatOptionalMeasurement(measurement.height))}</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="measurement-table-block">
|
||||
<div><span>${escapeHtml(formatDetailLabel("measurements"))}:</span></div>
|
||||
<table class="measurement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Canopy dia.</th>
|
||||
<th>Height</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function formatOptionalMeasurement(value) {
|
||||
return value === undefined ? "" : formatMetersAsFeet(value);
|
||||
}
|
||||
|
||||
function hasDatedMeasurements(measurements) {
|
||||
return getDatedMeasurements(measurements).length > 0;
|
||||
}
|
||||
|
||||
function getDatedMeasurements(measurements) {
|
||||
if (!Array.isArray(measurements)) {
|
||||
return [];
|
||||
}
|
||||
return measurements.filter((measurement) => measurement.measuredDate);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
|
||||
Reference in New Issue
Block a user