diff options
author | David Thompson <dthompson@vistahigherlearning.com> | 2022-01-21 11:42:28 -0500 |
---|---|---|
committer | David Thompson <dthompson@vistahigherlearning.com> | 2022-01-21 11:42:28 -0500 |
commit | bf0d6bc9600d2a5059e1a23add5eb64c11f0fa6e (patch) | |
tree | defd5cf1f45f540872189b961decf5ad0bb7050f /garden.js |
First commit!
Diffstat (limited to 'garden.js')
-rw-r--r-- | garden.js | 243 |
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); |