diff --git a/src/data/plants.js b/src/data/plants.js index 622c175..a894afa 100644 --- a/src/data/plants.js +++ b/src/data/plants.js @@ -116,7 +116,13 @@ const daylilies = [ }).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(1)), geo.south(geo.ft(4))), + }).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))), @@ -172,7 +178,7 @@ 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", @@ -193,23 +199,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))), ]; @@ -217,134 +219,121 @@ 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", pricePaid: 218, - }).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(3))), + }).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: [{ canopyDiameter: geo.ft(5.74), height: geo.ft(8.2) }], gardenOrgId: "79137", plantedDate: "2021-09-21", pricePaid: 218, - }).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(20))), + }).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: [{ canopyDiameter: geo.ft(5.74), height: geo.ft(8.2) }], gardenOrgId: "79137", plantedDate: "2021-09-21", pricePaid: 218, - }).add(geo.offset(lotCorner), geo.east(geo.ft(3)), geo.south(geo.ft(37))), + }).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)), ]; const bushes = [ new Tree("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", { 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", { 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", { 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", { 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", { 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", { 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", { species: "Spirea", - canopyDiameterMeters: 1.5, - heightMeters: 1, + measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }], }).add(geo.offset(lotCorner), geo.east(geo.ft(70)), geo.south(geo.ft(31))), new Tree("Spirea", { species: "Spirea", - canopyDiameterMeters: 1.5, - heightMeters: 1, + measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }], }).add(geo.offset(lotCorner), geo.east(geo.ft(74)), geo.south(geo.ft(31))), new Tree("Spirea", { species: "Spirea", - canopyDiameterMeters: 1.5, - heightMeters: 1, + measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }], }).add(geo.offset(lotCorner), geo.east(geo.ft(78)), geo.south(geo.ft(31))), ]; diff --git a/src/lib/feature-types.js b/src/lib/feature-types.js index 516dd29..91b145b 100644 --- a/src/lib/feature-types.js +++ b/src/lib/feature-types.js @@ -128,6 +128,9 @@ export const featureTypes = { species: "string", cultivar: "string", gardenOrgId: "string", + measurements: "plantMeasurement[]", + canopyDiameter: "number", + height: "number", note: "string", }, externalLinks: [ @@ -158,6 +161,9 @@ export const featureTypes = { cultivar: "string", bloomColor: "string", bloomSeason: "string", + measurements: "plantMeasurement[]", + canopyDiameter: "number", + height: "number", note: "string", }, style: { @@ -179,6 +185,9 @@ export const featureTypes = { cultivar: "string", bloomColor: "string", bloomSeason: "string", + measurements: "plantMeasurement[]", + canopyDiameter: "number", + height: "number", note: "string", }, style: { @@ -200,14 +209,15 @@ 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, }), @@ -221,14 +231,15 @@ 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, }), diff --git a/src/lib/yard-features.js b/src/lib/yard-features.js index 17880fd..f113c66 100644 --- a/src/lib/yard-features.js +++ b/src/lib/yard-features.js @@ -1,7 +1,7 @@ import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js"; import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js"; -export const YARD_SCHEMA_VERSION = 2; +export const YARD_SCHEMA_VERSION = 3; class YardFeature { constructor(kind, geometryType, name, details = {}) { @@ -286,7 +286,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)); } } @@ -324,6 +324,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,6 +396,11 @@ export function collectYardData(features, groups = []) { plantedDate: "date", removedDate: "date", }; + collection.measurementFields = { + measuredDate: "date", + canopyDiameter: "number", + height: "number", + }; collection.featureTypes = listFeatureTypeSchemas(); collection.groups = groups.map((group) => ({ id: group.id, diff --git a/src/main.js b/src/main.js index adc62d6..9abd1e6 100644 --- a/src/main.js +++ b/src/main.js @@ -305,7 +305,7 @@ function extendBounds(bounds, coordinates) { 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 = []; @@ -344,14 +344,23 @@ function buildPopupContent(properties) { return `${title}${subtitle}