Improvements to tree size measurements

This commit is contained in:
2026-07-07 21:43:04 -05:00
parent 07c6a9e390
commit da6371b53f
4 changed files with 169 additions and 64 deletions
+40 -51
View File
@@ -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))),
];
+17 -6
View File
@@ -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,
}),
+66 -2
View File
@@ -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,
+46 -5
View File
@@ -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}<div>${detailRows}${linkRows}</div>`;
}
function shouldShowDetailField(key) {
function shouldShowDetailField(key, details = {}) {
if (
Array.isArray(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",
@@ -373,7 +382,11 @@ function formatDetailLabel(key) {
}
function formatDetailValue(key, value) {
if (key === "canopyDiameterMeters" || key === "heightMeters") {
if (key === "measurements") {
return formatPlantMeasurements(value);
}
if (key === "canopyDiameter" || key === "height") {
return `${value} m`;
}
@@ -384,6 +397,34 @@ function formatDetailValue(key, value) {
return String(value);
}
function formatPlantMeasurements(measurements) {
if (!Array.isArray(measurements)) {
return String(measurements);
}
return measurements.map(formatPlantMeasurement).join("; ");
}
function formatPlantMeasurement(measurement) {
const parts = [];
if (measurement.measuredDate) {
parts.push(formatDetailValue("measuredDate", measurement.measuredDate));
}
if (measurement.canopyDiameter !== undefined) {
parts.push(
`canopy ${formatDetailValue("canopyDiameter", measurement.canopyDiameter)}`,
);
}
if (measurement.height !== undefined) {
parts.push(`height ${formatDetailValue("height", measurement.height)}`);
}
return parts.join(", ");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")