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: { color: "#7b684d", weight: 2, dashArray: "8 5", fillColor: "#dccaab", fillOpacity: 0.08, }, }, fence: { label: "Fence", categoryId: "structures", geometryType: "LineString", detailFields: { fenceType: "string", }, style: { color: "#705033", weight: 4, opacity: 0.95, }, }, yardGrid: { label: "Yard Grid", categoryId: "reference", geometryType: "LineString", detailFields: { axis: "string", offsetFeet: "number", spacingFeet: "number", }, style: (details) => ({ color: details.offsetFeet === 0 ? "#1f4f63" : "#2f7f98", weight: details.offsetFeet === 0 ? 3 : 2, opacity: details.offsetFeet === 0 ? 0.75 : 0.55, dashArray: details.offsetFeet === 0 ? undefined : "6 5", }), }, house: { label: "House", collectionLabel: "Houses", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#8d5a35", weight: 2, fillColor: "#c98d58", fillOpacity: 0.35, }, }, garage: { label: "Garage", collectionLabel: "Garages", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#60636b", weight: 2, fillColor: "#a4acb9", fillOpacity: 0.45, }, }, porch: { label: "Porch", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#aa7148", weight: 2, fillColor: "#deb588", fillOpacity: 0.38, }, }, deck: { label: "Deck", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#7f5a3c", weight: 2, fillColor: "#b98c66", fillOpacity: 0.32, }, }, walkway: { label: "Walkway", collectionLabel: "Walkways", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#b8b3a6", weight: 2, fillColor: "#d6d1c3", fillOpacity: 0.75, }, }, driveway: { label: "Driveway", collectionLabel: "Driveways", categoryId: "structures", geometryType: "Polygon", detailFields: {}, style: { color: "#9c9890", weight: 2, fillColor: "#bbb7af", fillOpacity: 0.8, }, }, flowerBed: { label: "Flower Bed", collectionLabel: "Flower Beds", categoryId: "structures", geometryType: "Polygon", detailFields: { soil: "string", mulch: "string", sunExposure: "string", }, style: { color: "#7f7f31", weight: 2, fillColor: "#bfc97b", fillOpacity: 0.38, }, }, plant: { label: "Plant", categoryId: "plants", geometryType: "Point", detailFields: { commonName: "string", genus: "string", species: "string", cultivar: "string", gardenOrgId: "string", measurements: "plantMeasurement[]", canopyDiameter: "number", height: "number", note: "string", }, externalLinks: [ { label: "Garden.org", url: (details) => details.gardenOrgId ? `https://garden.org/plants/view/${encodeURIComponent(details.gardenOrgId)}/` : null, }, ], style: { color: "#3d7140", weight: 2, radius: 5, fillColor: "#78a658", fillOpacity: 0.8, }, }, flower: { label: "Flower", collectionLabel: "Flowers", parentKind: "plant", geometryType: "Point", detailFields: { commonName: "string", genus: "string", species: "string", cultivar: "string", bloomColor: "string", bloomSeason: "string", measurements: "plantMeasurement[]", canopyDiameter: "number", height: "number", note: "string", }, style: { color: "#9c3f74", weight: 2, fillColor: "#d979aa", fillOpacity: 0.85, iconUrl: "./src/assets/icons/flower.svg", iconDiameterMeters: 0.4, }, }, daylily: { label: "Daylily", collectionLabel: "Daylilies", parentKind: "flower", geometryType: "Point", detailFields: { commonName: "string", genus: "string", cultivar: "string", bloomColor: "string", bloomSeason: "string", measurements: "plantMeasurement[]", canopyDiameter: "number", height: "number", note: "string", }, style: { color: "#9f5e1d", weight: 2, radius: 5, fillColor: "#f0b44c", fillOpacity: 0.88, iconUrl: "./src/assets/icons/daylily.svg", iconDiameterMeters: 2.5 / 3.280839895, }, }, shrub: { label: "Shrub", collectionLabel: "Shrubs", parentKind: "plant", geometryType: "Point", detailFields: { commonName: "string", genus: "string", species: "string", cultivar: "string", measurements: "plantMeasurement[]", canopyDiameter: "number", height: "number", note: "string", }, style: (details) => ({ color: "#426a3d", weight: 1, radiusMeters: (details.canopyDiameter ?? 1) / 2, fillColor: "#7e9d58", fillOpacity: 0.62, }), }, tree: { label: "Tree", collectionLabel: "Trees", parentKind: "plant", geometryType: "Point", detailFields: { commonName: "string", genus: "string", species: "string", cultivar: "string", measurements: "plantMeasurement[]", canopyDiameter: "number", height: "number", note: "string", }, style: (details) => ({ color: "#2f6b3d", weight: 1, radiusMeters: (details.canopyDiameter ?? 2) / 2, fillColor: "#5c9e64", fillOpacity: 0.55, }), }, sprinkler: { label: "Sprinkler", categoryId: "infrastructure", geometryType: "Point", detailFields: { zone: "string", headType: "string", flowRateGallonsPerMinute: "number", }, style: { color: "#2d7dd2", radius: 5, fillColor: "#88bdf2", 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) { const definition = featureTypes[kind]; if (!definition) { throw new Error(`Unknown feature type: ${kind}`); } return definition; } 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), externalLinks: getInheritedExternalLinks(kind).map((link) => ({ label: link.label, })), })); } 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 }; } export function resolveFeatureLinks(kind, details = {}) { return getInheritedExternalLinks(kind) .map((link) => ({ label: link.label, url: link.url(details), })) .filter((link) => link.url); } function getFeatureTypeLineage(kind) { const lineage = []; let currentKind = kind; while (currentKind) { const definition = getFeatureType(currentKind); lineage.unshift(definition); currentKind = definition.parentKind; } 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( {}, ...getFeatureTypeLineage(kind).map((definition) => definition.detailFields ?? {}), ); } function getInheritedExternalLinks(kind) { return getFeatureTypeLineage(kind).flatMap((definition) => definition.externalLinks ?? []); }