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))), }).add(geo.offset(lotCorner), geo.east(geo.ft(142)), geo.south(geo.ft(38))),
new Daylily("Extra daylily", { new Daylily("Extra daylily", {
gardenOrgId: "", 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", { new Daylily("Extra daylily", {
gardenOrgId: "", gardenOrgId: "",
}).add(geo.offset(lotCorner), geo.east(geo.ft(1)), geo.south(geo.ft(9))), }).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", { new Flower("Tiny Tortuga Turtlehead", {
gardenOrgId: "697986", gardenOrgId: "697986",
plantedDate: "2021-09-21", 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", { new Flower("Tiny Tortuga Turtlehead", {
gardenOrgId: "697986", gardenOrgId: "697986",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
@@ -193,23 +199,19 @@ const houseplants = [
const cutDownTrees = [ const cutDownTrees = [
new Tree("Maybe elm", { new Tree("Maybe elm", {
canopyDiameterMeters: 15, measurements: [{ canopyDiameter: geo.ft(49.21), height: geo.ft(49.21) }],
heightMeters: 15,
removedDate: "2021-05-05", removedDate: "2021-05-05",
}).add(geo.offset(lotCorner), geo.east(geo.ft(65)), geo.south(geo.ft(-2))), }).add(geo.offset(lotCorner), geo.east(geo.ft(65)), geo.south(geo.ft(-2))),
new Tree("Big pine tree", { new Tree("Big pine tree", {
canopyDiameterMeters: 8, measurements: [{ canopyDiameter: geo.ft(26.25), height: geo.ft(49.21) }],
heightMeters: 15,
removedDate: "2021-05-06", removedDate: "2021-05-06",
}).add(geo.offset(lotCorner), geo.east(geo.ft(98)), geo.south(geo.ft(-15))), }).add(geo.offset(lotCorner), geo.east(geo.ft(98)), geo.south(geo.ft(-15))),
new Tree("Walnut by house", { new Tree("Walnut by house", {
canopyDiameterMeters: 6, measurements: [{ canopyDiameter: geo.ft(19.69), height: geo.ft(22.97) }],
heightMeters: 7,
removedDate: "2021-05-01", removedDate: "2021-05-01",
}).add(geo.offset(lotCorner), geo.east(geo.ft(32)), geo.south(geo.ft(40))), }).add(geo.offset(lotCorner), geo.east(geo.ft(32)), geo.south(geo.ft(40))),
new Tree("Walnut by shed", { new Tree("Walnut by shed", {
canopyDiameterMeters: 7, measurements: [{ canopyDiameter: geo.ft(22.97), height: geo.ft(22.97) }],
heightMeters: 7,
removedDate: "2021-05-01", removedDate: "2021-05-01",
}).add(geo.offset(lotCorner), geo.east(geo.ft(100)), geo.south(geo.ft(40))), }).add(geo.offset(lotCorner), geo.east(geo.ft(100)), geo.south(geo.ft(40))),
]; ];
@@ -217,134 +219,121 @@ const cutDownTrees = [
const trees = [ const trees = [
new Tree("Northern Empress Japanese Elm", { new Tree("Northern Empress Japanese Elm", {
species: "Northern Empress Japanese Elm", species: "Northern Empress Japanese Elm",
canopyDiameterMeters: 6, measurements: [{ canopyDiameter: geo.ft(19.69), height: geo.ft(22.97) }],
heightMeters: 7,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 312, pricePaid: 312,
}).add(geo.offset(lotCorner), geo.east(geo.ft(9)), geo.south(geo.ft(16))), }).add(geo.offset(lotCorner), geo.east(geo.ft(9)), geo.south(geo.ft(16))),
new Tree("Hot Wings Maple", { new Tree("Hot Wings Maple", {
species: "Hot Wings Maple", species: "Hot Wings Maple",
canopyDiameterMeters: 5, measurements: [{ canopyDiameter: geo.ft(16.4), height: geo.ft(16.4) }],
heightMeters: 5,
gardenOrgId: "536120", gardenOrgId: "536120",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 186, pricePaid: 186,
}).add(geo.offset(lotCorner), geo.east(geo.ft(60)), geo.south(geo.ft(40))), }).add(geo.offset(lotCorner), geo.east(geo.ft(60)), geo.south(geo.ft(40))),
new Tree("Prairie Expedition American Elm", { new Tree("Prairie Expedition American Elm", {
species: "Prairie Expedition American Elm", species: "Prairie Expedition American Elm",
canopyDiameterMeters: 5, measurements: [{ canopyDiameter: geo.ft(16.4), height: geo.ft(16.4) }],
heightMeters: 5,
gardenOrgId: "736267", gardenOrgId: "736267",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 280, pricePaid: 280,
}).add(geo.offset(lotCorner), geo.east(geo.ft(95)), geo.south(geo.ft(15))), }).add(geo.offset(lotCorner), geo.east(geo.ft(95)), geo.south(geo.ft(15))),
new Tree("Dwarf Korean Lilac", { new Tree("Dwarf Korean Lilac", {
species: "Dwarf Korean Lilac", species: "Dwarf Korean Lilac",
canopyDiameterMeters: 1.75, measurements: [
heightMeters: 2.5, { measuredDate: "2026-07-08", canopyDiameter: geo.ft(5.5), height: geo.ft(6.7) },
],
gardenOrgId: "79137", gardenOrgId: "79137",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 218, 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", { new Tree("Dwarf Korean Lilac", {
species: "Dwarf Korean Lilac", species: "Dwarf Korean Lilac",
canopyDiameterMeters: 1.75, measurements: [{ canopyDiameter: geo.ft(5.74), height: geo.ft(8.2) }],
heightMeters: 2.5,
gardenOrgId: "79137", gardenOrgId: "79137",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 218, 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", { new Tree("Dwarf Korean Lilac", {
species: "Dwarf Korean Lilac", species: "Dwarf Korean Lilac",
canopyDiameterMeters: 1.75, measurements: [{ canopyDiameter: geo.ft(5.74), height: geo.ft(8.2) }],
heightMeters: 2.5,
gardenOrgId: "79137", gardenOrgId: "79137",
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 218, 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", { new Tree("Boulevard Linden", {
species: "Boulevard Linden", species: "Boulevard Linden",
canopyDiameterMeters: 3, measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(19.69) }],
heightMeters: 6,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 327, pricePaid: 327,
}).add(geo.lngLat(-100.8990003556445, 46.826776648309774)), }).add(geo.lngLat(-100.8990003556445, 46.826776648309774)),
new Tree("Boulevard Linden 2", { new Tree("Boulevard Linden 2", {
species: "Boulevard Linden", species: "Boulevard Linden",
canopyDiameterMeters: 3, measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(19.69) }],
heightMeters: 6,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 327, pricePaid: 327,
}).add(geo.lngLat(-100.89898269567485, 46.826709328342595)), }).add(geo.lngLat(-100.89898269567485, 46.826709328342595)),
new Tree("Evergreen", { new Tree("Evergreen", {
species: "Evergreen", species: "Evergreen",
canopyDiameterMeters: 8, measurements: [{ canopyDiameter: geo.ft(26.25), height: geo.ft(49.21) }],
heightMeters: 15,
}).add(geo.lngLat(-100.89888268258135, 46.826680107011924)), }).add(geo.lngLat(-100.89888268258135, 46.826680107011924)),
]; ];
const bushes = [ const bushes = [
new Tree("Medora Juniper 1", { new Tree("Medora Juniper 1", {
species: "Medora Juniper", species: "Medora Juniper",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
heightMeters: 2.5,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 96.50, pricePaid: 96.50,
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89880933964611, 46.826705259550664)), }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89880933964611, 46.826705259550664)),
new Tree("Medora Juniper 2", { new Tree("Medora Juniper 2", {
species: "Medora Juniper", species: "Medora Juniper",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
heightMeters: 2.5,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 96.50, pricePaid: 96.50,
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89877004677517, 46.82670396593881)), }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89877004677517, 46.82670396593881)),
new Tree("Medora Juniper 3", { new Tree("Medora Juniper 3", {
species: "Medora Juniper", species: "Medora Juniper",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
heightMeters: 2.5,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 96.50, pricePaid: 96.50,
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.898799106227, 46.82669830398086)), }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.898799106227, 46.82669830398086)),
new Tree("Medora Juniper 4", { new Tree("Medora Juniper 4", {
species: "Medora Juniper", species: "Medora Juniper",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
heightMeters: 2.5,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 96.50, pricePaid: 96.50,
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89878871410515, 46.82669251034885)), }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.89878871410515, 46.82669251034885)),
new Tree("Medora Juniper 5", { new Tree("Medora Juniper 5", {
species: "Medora Juniper", species: "Medora Juniper",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(8.2) }],
heightMeters: 2.5,
plantedDate: "2021-09-21", plantedDate: "2021-09-21",
pricePaid: 96.50, pricePaid: 96.50,
}).inGroup(juniperTreesGroup).add(geo.lngLat(-100.8987812086838, 46.82671120797719)), }).inGroup(juniperTreesGroup).add(geo.lngLat(-100.8987812086838, 46.82671120797719)),
new Tree("Black Currant", { new Tree("Black Currant", {
species: "Black Currant", species: "Black Currant",
canopyDiameterMeters: 2, measurements: [{ canopyDiameter: geo.ft(6.56), height: geo.ft(6.56) }],
heightMeters: 2,
gardenOrgId: "87767", gardenOrgId: "87767",
}).add(geo.offset(lotCorner), geo.east(geo.ft(107)), geo.south(geo.ft(3))), }).add(geo.offset(lotCorner), geo.east(geo.ft(107)), geo.south(geo.ft(3))),
new Tree("Elderberry", { new Tree("Elderberry", {
species: "Elderberry", species: "Elderberry",
canopyDiameterMeters: 3, measurements: [{ canopyDiameter: geo.ft(9.84), height: geo.ft(9.84) }],
heightMeters: 3,
gardenOrgId: "78882", gardenOrgId: "78882",
}).add(geo.offset(lotCorner), geo.east(geo.ft(142)), geo.south(geo.ft(52))), }).add(geo.offset(lotCorner), geo.east(geo.ft(142)), geo.south(geo.ft(52))),
new Tree("Spirea", { new Tree("Spirea", {
species: "Spirea", species: "Spirea",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
heightMeters: 1,
}).add(geo.offset(lotCorner), geo.east(geo.ft(70)), geo.south(geo.ft(31))), }).add(geo.offset(lotCorner), geo.east(geo.ft(70)), geo.south(geo.ft(31))),
new Tree("Spirea", { new Tree("Spirea", {
species: "Spirea", species: "Spirea",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
heightMeters: 1,
}).add(geo.offset(lotCorner), geo.east(geo.ft(74)), geo.south(geo.ft(31))), }).add(geo.offset(lotCorner), geo.east(geo.ft(74)), geo.south(geo.ft(31))),
new Tree("Spirea", { new Tree("Spirea", {
species: "Spirea", species: "Spirea",
canopyDiameterMeters: 1.5, measurements: [{ canopyDiameter: geo.ft(4.92), height: geo.ft(3.28) }],
heightMeters: 1,
}).add(geo.offset(lotCorner), geo.east(geo.ft(78)), geo.south(geo.ft(31))), }).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", species: "string",
cultivar: "string", cultivar: "string",
gardenOrgId: "string", gardenOrgId: "string",
measurements: "plantMeasurement[]",
canopyDiameter: "number",
height: "number",
note: "string", note: "string",
}, },
externalLinks: [ externalLinks: [
@@ -158,6 +161,9 @@ export const featureTypes = {
cultivar: "string", cultivar: "string",
bloomColor: "string", bloomColor: "string",
bloomSeason: "string", bloomSeason: "string",
measurements: "plantMeasurement[]",
canopyDiameter: "number",
height: "number",
note: "string", note: "string",
}, },
style: { style: {
@@ -179,6 +185,9 @@ export const featureTypes = {
cultivar: "string", cultivar: "string",
bloomColor: "string", bloomColor: "string",
bloomSeason: "string", bloomSeason: "string",
measurements: "plantMeasurement[]",
canopyDiameter: "number",
height: "number",
note: "string", note: "string",
}, },
style: { style: {
@@ -200,14 +209,15 @@ export const featureTypes = {
genus: "string", genus: "string",
species: "string", species: "string",
cultivar: "string", cultivar: "string",
canopyDiameterMeters: "number", measurements: "plantMeasurement[]",
heightMeters: "number", canopyDiameter: "number",
height: "number",
note: "string", note: "string",
}, },
style: (details) => ({ style: (details) => ({
color: "#426a3d", color: "#426a3d",
weight: 1, weight: 1,
radiusMeters: (details.canopyDiameterMeters ?? 1) / 2, radiusMeters: (details.canopyDiameter ?? 1) / 2,
fillColor: "#7e9d58", fillColor: "#7e9d58",
fillOpacity: 0.62, fillOpacity: 0.62,
}), }),
@@ -221,14 +231,15 @@ export const featureTypes = {
genus: "string", genus: "string",
species: "string", species: "string",
cultivar: "string", cultivar: "string",
canopyDiameterMeters: "number", measurements: "plantMeasurement[]",
heightMeters: "number", canopyDiameter: "number",
height: "number",
note: "string", note: "string",
}, },
style: (details) => ({ style: (details) => ({
color: "#2f6b3d", color: "#2f6b3d",
weight: 1, weight: 1,
radiusMeters: (details.canopyDiameterMeters ?? 2) / 2, radiusMeters: (details.canopyDiameter ?? 2) / 2,
fillColor: "#5c9e64", fillColor: "#5c9e64",
fillOpacity: 0.55, fillOpacity: 0.55,
}), }),
+66 -2
View File
@@ -1,7 +1,7 @@
import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js"; import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js"; import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js";
export const YARD_SCHEMA_VERSION = 2; export const YARD_SCHEMA_VERSION = 3;
class YardFeature { class YardFeature {
constructor(kind, geometryType, name, details = {}) { constructor(kind, geometryType, name, details = {}) {
@@ -286,7 +286,7 @@ export class FlowerBed extends PolygonFeature {
export class Plant extends PointFeature { export class Plant extends PointFeature {
constructor(name = "Plant", details = {}, kind = "plant") { 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 { export class Sprinkler extends PointFeature {
constructor(name = "Sprinkler", details = {}) { constructor(name = "Sprinkler", details = {}) {
super("sprinkler", name, details); super("sprinkler", name, details);
@@ -337,6 +396,11 @@ export function collectYardData(features, groups = []) {
plantedDate: "date", plantedDate: "date",
removedDate: "date", removedDate: "date",
}; };
collection.measurementFields = {
measuredDate: "date",
canopyDiameter: "number",
height: "number",
};
collection.featureTypes = listFeatureTypeSchemas(); collection.featureTypes = listFeatureTypeSchemas();
collection.groups = groups.map((group) => ({ collection.groups = groups.map((group) => ({
id: group.id, id: group.id,
+46 -5
View File
@@ -305,7 +305,7 @@ function extendBounds(bounds, coordinates) {
function buildPopupContent(properties) { function buildPopupContent(properties) {
const { kind, label, name, plantedDate, removedDate, details, parentId, groupIds } = properties; const { kind, label, name, plantedDate, removedDate, details, parentId, groupIds } = properties;
const entries = Object.entries(details ?? {}).filter( 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 lifecycleEntries = [];
const relationshipEntries = []; const relationshipEntries = [];
@@ -344,14 +344,23 @@ function buildPopupContent(properties) {
return `${title}${subtitle}<div>${detailRows}${linkRows}</div>`; 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"; return !key.endsWith("Url") && key !== "gardenOrgId";
} }
function formatDetailLabel(key) { function formatDetailLabel(key) {
const labels = { const labels = {
canopyDiameterMeters: "Canopy diameter", canopyDiameter: "Canopy diameter",
heightMeters: "Height", height: "Height",
measurements: "Measurements",
measuredDate: "Measured",
plantedDate: "Planted", plantedDate: "Planted",
removedDate: "Removed", removedDate: "Removed",
commonName: "Common name", commonName: "Common name",
@@ -373,7 +382,11 @@ function formatDetailLabel(key) {
} }
function formatDetailValue(key, value) { function formatDetailValue(key, value) {
if (key === "canopyDiameterMeters" || key === "heightMeters") { if (key === "measurements") {
return formatPlantMeasurements(value);
}
if (key === "canopyDiameter" || key === "height") {
return `${value} m`; return `${value} m`;
} }
@@ -384,6 +397,34 @@ function formatDetailValue(key, value) {
return String(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) { function escapeHtml(value) {
return String(value) return String(value)
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")