summaryrefslogtreecommitdiff
path: root/garden.js
diff options
context:
space:
mode:
authorDavid Thompson <dthompson@vistahigherlearning.com>2022-01-21 11:42:28 -0500
committerDavid Thompson <dthompson@vistahigherlearning.com>2022-01-21 11:42:28 -0500
commitbf0d6bc9600d2a5059e1a23add5eb64c11f0fa6e (patch)
treedefd5cf1f45f540872189b961decf5ad0bb7050f /garden.js
First commit!
Diffstat (limited to 'garden.js')
-rw-r--r--garden.js243
1 files changed, 243 insertions, 0 deletions
diff --git a/garden.js b/garden.js
new file mode 100644
index 0000000..f1e6064
--- /dev/null
+++ b/garden.js
@@ -0,0 +1,243 @@
+class Crop {
+ constructor(name, {fallPlanting, interval, maturity, multisow,
+ spacing, springPlant, springStart, trellis} =
+ {interval: 0, maturity: 0, multisow: 1, spacing: 1,
+ trellis: false}) {
+ this.fallPlanting = fallPlanting; // 'sow', 'transplant', or nothing
+ this.interval = interval; // succession planting interval (weeks)
+ this.maturity = maturity; // average time to maturity from transplant/seed (weeks)
+ this.multisow = multisow; // seeds per module
+ this.name = name;
+ this.spacing = spacing; // per square foot
+ this.springPlant = springPlant; // direct sow/transplant (week offset from last frost)
+ this.springStart = springStart; // start indoors (week offset from last frost)
+ this.trellis = trellis; // needs a trellis?
+ }
+
+ isSpringTransplant() {
+ return Number.isInteger(this.springStart);
+ }
+}
+
+const crops = [
+ new Crop("Arugula", {interval: 2, maturity: 6, spacing: 4, springPlant: -4}),
+ new Crop("Basil", {maturity: 6, springStart: -6, springPlant: 2}),
+ new Crop("Bean (Bush)", {interval: 2, maturity: 8, spacing: 9, springPlant: 1}),
+ new Crop("Bean (Pole)", {maturity: 9, spacing: 8, springPlant: 1, trellis: true}),
+ new Crop("Beet", {interval: 2, maturity: 8, multisow: 4, spacing: 4, springPlant: -3}),
+ new Crop("Bok Choy", {fallPlanting: 'sow', maturity: 7, spacing: 4, springStart: -6, springPlant: -2}),
+ new Crop("Broccoli", {fallPlanting: 'transplant', maturity: 12, springStart: -7, springPlant: -3}),
+ new Crop("Brussels Sprouts", {fallPlanting: 'transplant', maturity: 16}),
+ new Crop("Cabbage (Early)", {maturity: 6, springStart: -7, springPlant: -3}),
+ new Crop("Cabbage (Late)", {fallPlanting: 'transplant', maturity: 14, springStart: -7, springPlant: -3}),
+ new Crop("Cabbage (Asian)", {fallPlanting: 'transplant', maturity: 10, springStart: -7, springPlant: -3}),
+ new Crop("Cauliflower", {fallPlanting: 'transplant', maturity: 10, springStart: -7, springPlant: -3}),
+ new Crop("Carrot", {interval: 2, maturity: 10, spacing: 16, springPlant: -4}),
+ new Crop("Cilantro", {interval: 2, maturity: 7, springPlant: 0}),
+ new Crop("Claytonia", {fallPlanting: 'sow', maturity: 6, spacing: 9, springPlant: -5}),
+ new Crop("Cucumber (Vine)", {maturity: 9, spacing: 2, springPlant: 1, trellis: true}),
+ new Crop("Dill", {interval: 4, maturity: 8, springPlant: 0}),
+ new Crop("Eggplant", {maturity: 11, springStart: -5, springPlant: 3}),
+ new Crop("Endive", {maturity: 4, springStart: -9, springPlant: -3}),
+ new Crop("Fennel", {fallPlanting: 'sow', maturity: 10, springPlant: 0}),
+ new Crop("Kale", {maturity: 4, springStart: -10, springPlant: -4}),
+ new Crop("Lettuce (Leaf/Mini Head)", {interval: 2, maturity: 7, spacing: 4, springStart: -8, springPlant: -4}),
+ new Crop("Mache", {interval: 2, maturity: 7, spacing: 16, springPlant: -4}),
+ new Crop("Mustard (Asian Greens)", {interval: 2, maturity: 6, spacing: 4, springStart: -7, springPlant: -4}),
+ new Crop("Onion", {maturity: 8, multisow: 3, spacing: 4, springStart: -10, springPlant: -4}),
+ new Crop("Parsley", {maturity: 7, spacing: 4, springStart: -10, springPlant: -4}),
+ new Crop("Parsnip", {maturity: 14, spacing: 4, springPlant: -2}),
+ new Crop("Pea", {fallPlanting: 'sow', maturity: 12, spacing: 8, springPlant: -4, trellis: true}),
+ new Crop("Pepper", {maturity: 10, springStart: -5, springPlant: 3}),
+ new Crop("Potato", {maturity: 12, spacing: 4, springPlant: -3}),
+ new Crop("Radish (Round)", {interval: 2, maturity: 4, multisow: 4, spacing: 4, springPlant: -3}),
+ new Crop("Radish (Daikon)", {fallPlanting: 'sow', maturity: 9, spacing: 4}),
+ new Crop("Shallot", {maturity: 9, multisow: 3, spacing: 4, springStart: -10, springPlant: -4}),
+ new Crop("Spinach", {interval: 2, maturity: 6, multisow: 4, spacing: 4, springPlant: -5}),
+ new Crop("Squash (Summer)", {maturity: 8, spacing: 1.0 / 9, springPlant: 0}),
+ new Crop("Squash (Winter)", {maturity: 12, spacing: 0.5, springPlant: 1}),
+ new Crop("Swiss Chard", {maturity: 4, spacing: 4, springStart: -8, springPlant: -3}),
+ new Crop("Tomato (Indeterminate)", {maturity: 11, springStart: -4, springPlant: 3, trellis: true}),
+ new Crop("Turnip (Salad)", {interval: 2, maturity: 5, spacing: 16, springPlant: -3})
+];
+
+function daysLater(date, n) {
+ return new Date(date.getTime() + n * 24 * 60 * 60 * 1000);
+}
+
+function weeksLater(date, n) {
+ return daysLater(date, n * 7);
+}
+
+function monthAndDay(date) {
+ return (date.getUTCMonth() + 1) + "/" + date.getUTCDate();
+}
+
+function range(start, end) {
+ const size = end - start;
+ const array = new Array(size);
+ for(let i = start; i < end; i++) {
+ array[i - start] = i;
+ }
+ return array;
+}
+
+function clearElement(element) {
+ while (element.firstChild) {
+ element.removeChild(element.firstChild);
+ }
+}
+
+function seasonLengthInWeeks(lastFrostDate, firstFrostDate) {
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
+ return (firstFrostDate.getTime() - lastFrostDate.getTime()) / oneWeek;
+}
+
+function springWeeks(lastFrostDate, firstFrostDate) {
+ const seasonLength = seasonLengthInWeeks(lastFrostDate, firstFrostDate);
+ return range(-10, seasonLength + 1).map(offset => {
+ return weeksLater(lastFrostDate, offset);
+ });
+}
+
+function createWeeksRow(weeks, lastFrostDate, firstFrostDate) {
+ const row = document.createElement('tr');
+ row.appendChild(document.createElement('td'));
+ weeks.forEach(date => {
+ const td = document.createElement('th', {scope: "col"});
+ const firstFrost = date.getTime() == firstFrostDate.getTime();
+ const lastFrost = date.getTime() == lastFrostDate.getTime() ;
+ const dateString = monthAndDay(date);
+ const label = firstFrost || lastFrost ? `${dateString}❄`: dateString;
+ const text = document.createTextNode(label);
+ td.appendChild(text);
+ row.appendChild(td);
+ });
+ return row;
+}
+
+function createHeader(headers) {
+ const thead = document.createElement('thead');
+ const row = document.createElement('tr');
+ headers.forEach(column => {
+ const th = document.createElement('th', {scope: "col"});
+ const text = document.createTextNode(column);
+ th.appendChild(text);
+ row.appendChild(th);
+ });
+ thead.appendChild(row);
+ return thead;
+}
+
+function createCropRow(crop, lastFrostDate, seasonLength) {
+ function isSuccession(offset) {
+ return crop.interval > 0 &&
+ offset >= crop.springPlant &&
+ // 2 weeks are subtracted to account for shortening days in the
+ // fall.
+ offset <= seasonLength - crop.maturity - 2 &&
+ (offset - crop.springPlant) % crop.interval == 0;
+ }
+
+ function icon(name) {
+ const img = document.createElement('img');
+ img.classList.add("icon");
+ img.src = `images/${name}.svg`;
+ img.title = {
+ "direct-sow": "direct sow",
+ "start-indoors": "start indoors",
+ transplant: "transplant outdoors"
+ }[name];
+ return img;
+ }
+
+ function sow() {
+ return icon('direct-sow');
+ }
+
+ function start() {
+ return icon('start-indoors');
+ }
+
+ function transplant() {
+ return icon('transplant');
+ }
+
+ function nothing() {
+ return null;
+ }
+
+ const actions = range(-10, seasonLength + 1).map(offset => {
+ function action(type) {
+ const date = weeksLater(lastFrostDate, offset);
+ const iconFunc = {
+ none: nothing,
+ start: start,
+ sow: sow,
+ transplant: transplant
+ }[type];
+ return {date: date, icon: iconFunc()};
+ }
+
+
+ if(offset == crop.springStart) {
+ return action('start');
+ } else if(offset == crop.springPlant) {
+ return crop.isSpringTransplant() ? action('transplant') : action('sow');
+ } else if(isSuccession(offset)) {
+ return action('sow');
+ } else if(crop.fallPlanting == 'sow' && offset == seasonLength - crop.maturity - 2) {
+ return action('sow');
+ } else if(crop.fallPlanting == 'transplant' && offset == seasonLength - crop.maturity - 6) {
+ return action('start');
+ } else if(crop.fallPlanting == 'transplant' && offset == seasonLength - crop.maturity - 2) {
+ return action('transplant');
+ }
+
+ return action('none');
+ });
+ const row = document.createElement('tr');
+ const th = document.createElement('td');
+ th.appendChild(document.createTextNode(crop.name));
+ row.appendChild(th);
+ actions.forEach(column => {
+ const td = document.createElement('td');
+ //const text = document.createTextNode(column.text);
+ td.title = monthAndDay(column.date);
+ if(column.icon) {
+ td.appendChild(column.icon);
+ }
+ row.appendChild(td);
+ });
+ return row;
+}
+
+function refreshTable() {
+ const headers = ["Crop", "Week"];
+ const scheduleTable = document.querySelector("#schedule");
+ const lastFrostDateInput = document.querySelector("input[name='last-frost-date']");
+ const firstFrostDateInput = document.querySelector("input[name='first-frost-date']");
+ const lastFrostDate = lastFrostDateInput.valueAsDate;
+ const firstFrostDate = firstFrostDateInput.valueAsDate;
+ if(firstFrostDate && lastFrostDate) {
+ const weeks = springWeeks(lastFrostDate, firstFrostDate);
+ const tbody = document.createElement('tbody');
+ clearElement(scheduleTable);
+ scheduleTable.appendChild(createHeader(headers));
+ tbody.appendChild(createWeeksRow(weeks, lastFrostDate, firstFrostDate));
+ crops.forEach(crop => {
+ tbody.appendChild(createCropRow(crop, lastFrostDate, weeks.length - 11));
+ });
+ scheduleTable.appendChild(tbody);
+ }
+}
+
+function loadIntensiveGarden() {
+ const lastFrostDateInput = document.querySelector("input[name='last-frost-date']");
+ lastFrostDateInput.addEventListener('input', event => {
+ refreshTable();
+ });
+ refreshTable();
+}
+
+window.addEventListener('load', loadIntensiveGarden);