Improvements to tree size measurements
This commit is contained in:
+40
-51
@@ -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))),
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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
@@ -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("&", "&")
|
||||
|
||||
Reference in New Issue
Block a user