diff options
author | David Thompson <dthompson@vistahigherlearning.com> | 2022-01-22 07:46:23 -0500 |
---|---|---|
committer | David Thompson <dthompson@vistahigherlearning.com> | 2022-01-22 07:46:23 -0500 |
commit | 592bc33aa3c03e15d0deccfe8dcfd4188da2015d (patch) | |
tree | 1634044148a81a9886bab153d10fafabd976c575 | |
parent | bf0d6bc9600d2a5059e1a23add5eb64c11f0fa6e (diff) |
Separate view and data layer, add more crop details, simplify Crop class.
-rw-r--r-- | css/garden.css | 52 | ||||
-rw-r--r-- | garden.js | 815 | ||||
-rw-r--r-- | index.html | 34 |
3 files changed, 683 insertions, 218 deletions
diff --git a/css/garden.css b/css/garden.css index e126ec9..2077bd6 100644 --- a/css/garden.css +++ b/css/garden.css @@ -352,21 +352,21 @@ template { @font-face { font-family: 'Linux Libertine'; - src: url('/fonts/LinLibertine_R.woff'); + src: url('../fonts/LinLibertine_R.woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Linux Libertine'; - src: url('/fonts/LinLibertine_RI.woff'); + src: url('../fonts/LinLibertine_RI.woff'); font-weight: normal; font-style: italic; } @font-face { font-family: 'Linux Libertine'; - src: url('/fonts/LinLibertine_RB.woff'); + src: url('../fonts/LinLibertine_RB.woff'); font-weight: bold; font-style: normal; } @@ -375,21 +375,21 @@ template { @font-face { font-family: 'Linux Biolinum'; - src: url('/fonts/linbio-r-subset.woff'); + src: url('../fonts/linbio-r-subset.woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'Linux Biolinum'; - src: url('/fonts/linbio-ri-subset.woff'); + src: url('../fonts/linbio-ri-subset.woff'); font-weight: normal; font-style: italic; } @font-face { font-family: 'Linux Biolinum'; - src: url('/fonts/linbio-rb-subset.woff'); + src: url('../fonts/linbio-rb-subset.woff'); font-weight: bold; font-style: normal; } @@ -403,7 +403,6 @@ body { font-family: 'Linux Libertine',serif; line-height: 140%; text-rendering: optimizeLegibility; - opacity: 87%; } @media (min-width: 1140px) { @@ -442,7 +441,7 @@ th, td { margin: 0; } -tbody tr:nth-child(even) { +tbody tr:nth-child(odd) { background-color: #306082; } @@ -467,10 +466,6 @@ img.key { max-height: 24px; } -.icon-direct-sow { - background-image: url("images/direct-sow.svg"); -} - a { color: #639bff; } @@ -478,3 +473,36 @@ a { a:visited { color: #847e87; } + +.crop { + color: #fff; +} + +.crop:hover { + cursor: pointer; +} + +.overlay { + position: fixed; + left: 0; + top: 0; + z-index: 9; + width: 100%; + height: 100%; + background-color: #000000bb; +} + +.modal { + position: fixed; + left: 40%; + top: 25%; + width: 20%; + height: 50%; + border-radius: 4px; + background-color: #121212; + padding: 2em; +} + +.detail-subheading { + opacity: 60%; +} @@ -1,77 +1,5 @@ -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(); -} +const WEEKS_BEFORE_LAST_FROST = 10; +const FALL_FACTOR = 2; function range(start, end) { const size = end - start; @@ -82,162 +10,667 @@ function range(start, end) { return array; } -function clearElement(element) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } +function daysLater(date, n) { + return new Date(date.getTime() + n * 24 * 60 * 60 * 1000); +} + +function weeksLater(date, n) { + return daysLater(date, n * 7); } function seasonLengthInWeeks(lastFrostDate, firstFrostDate) { const oneWeek = 7 * 24 * 60 * 60 * 1000; - return (firstFrostDate.getTime() - lastFrostDate.getTime()) / oneWeek; + const dt = firstFrostDate.getTime() - lastFrostDate.getTime(); + return Math.floor(dt / oneWeek); } -function springWeeks(lastFrostDate, firstFrostDate) { - const seasonLength = seasonLengthInWeeks(lastFrostDate, firstFrostDate); - return range(-10, seasonLength + 1).map(offset => { - return weeksLater(lastFrostDate, offset); +function seasonWeeks(lastFrostDate, seasonLength) { + return range(-WEEKS_BEFORE_LAST_FROST, seasonLength + 1).map(offset => { + return { + date: weeksLater(lastFrostDate, offset), + offset: 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; +class Season { + constructor(lastFrostDate, firstFrostDate) { + this.lastFrostDate = lastFrostDate; + this.firstFrostDate = firstFrostDate; + this.length = seasonLengthInWeeks(lastFrostDate, firstFrostDate); + this.weeks = seasonWeeks(lastFrostDate, this.length); + } } -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; +class Crop { + constructor(name, {fallCrop, headStart, interval, maturity, multisow, + pause, spacing, springPlant, resume, species, + transplant, trellis}) { + // Plant again in the fall? + this.fallCrop = fallCrop; + // Weeks spent indoors before transplanting. + this.headStart = headStart || 0; + // Succession planting interval (weeks) + this.interval = interval || 0; + // Average time to maturity from transplanting or sowing (weeks) + this.maturity = maturity; + // Seeds per module + this.multisow = multisow || 1; + // Human readable name + this.name = name; + // When to pause succession planting (weeks after last frost) + this.pause = pause || 99; + // When to resume succession planting (weeks before first frost) + this.resume = resume || 99; + // Plants per square foot, or, in the case of multisowing, clumps + // per square foot. + this.spacing = spacing || 1; + // Latin names. + this.species = species || []; + // Earliest sowing/transplanting week (offset relative to last + // frost date.) + this.springPlant = springPlant; + // Transplanted or direct sown? + this.transplant = transplant || false; + // Needs a trellis? + this.trellis = trellis || false; + } } -function createCropRow(crop, lastFrostDate, seasonLength) { +const CROPS = [ + new Crop("Arugula", { + interval: 2, + maturity: 6, + pause: 3, + resume: 11, + spacing: 4, + species: ["eruca sativa"], + springPlant: -4 + }), + new Crop("Basil", { + headStart: 8, + maturity: 6, + species: ["ocimum basilicum"], + springPlant: 2, + transplant: true + }), + new Crop("Bean (Bush)", { + interval: 2, + maturity: 8, + spacing: 9, + species: ["phaseolus vulgaris"], + springPlant: 1 + }), + new Crop("Bean (Pole)", { + maturity: 9, + spacing: 8, + species: ["phaseolus vulgaris"], + springPlant: 1, + trellis: true + }), + new Crop("Beet", { + interval: 2, + maturity: 8, + multisow: 4, + pause: 1, + resume: 12, + spacing: 4, + species: ["beta vulgaris"], + springPlant: -3 + }), + new Crop("Bok Choy", { + fallCrop: true, + headStart: 4, + maturity: 7, + spacing: 4, + species: ["brassica rapa"], + springPlant: -2 + }), + new Crop("Broccoli", { + fallCrop: true, + headStart: 4, + maturity: 12, + species: ["brassica oleracea"], + springPlant: -3, + transplant: true + }), + new Crop("Brussels Sprouts", { + headStart: 4, + fallCrop: true, + maturity: 16, + species: ["brassica oleracea"], + transplant: true + }), + new Crop("Cabbage (Early)", { + headStart: 4, + maturity: 6, + species: ["brassica oleracea"], + springPlant: -3, + transplant: true + }), + new Crop("Cabbage (Late)", { + fallCrop: true, + headStart: 4, + maturity: 14, + species: ["brassica oleracea"], + springPlant: -3, + transplant: true + }), + new Crop("Cabbage (Asian)", { + fallCrop: true, + headStart: 4, + maturity: 10, + species: ["brassica rapa"], + springPlant: -3, + transplant: true + }), + new Crop("Cauliflower", { + fallCrop: true, + headStart: 4, + maturity: 10, + species: ["brassica oleracea"], + springPlant: -3, + transplant: true + }), + new Crop("Carrot", { + interval: 2, + maturity: 10, + spacing: 16, + species: ["daucus carota"], + springPlant: -4 + }), + new Crop("Cilantro", { + interval: 2, + maturity: 7, + species: ["coriandrum sativum"], + springPlant: 0 + }), + new Crop("Claytonia", { + fallCrop: true, + maturity: 6, + spacing: 9, + species: ["montia perfoliata"], + springPlant: -5 + }), + new Crop("Cucumber (Vine)", { + maturity: 9, + spacing: 2, + species: ["cucumis sativus"], + springPlant: 1, + trellis: true + }), + new Crop("Dill", { + interval: 4, + maturity: 8, + species: ["anethum graveolens"], + springPlant: 0 + }), + new Crop("Eggplant", { + headStart: 8, + maturity: 11, + species: ["solanum melongena"], + springPlant: 3, + transplant: true + }), + new Crop("Endive", { + headStart: 6, + maturity: 4, + species: ["cichorium endivia"], + springPlant: -3, + transplant: true + }), + new Crop("Fennel", { + fallCrop: 'sow', + maturity: 10, + species: ["foeniculum vulgare"], + springPlant: 0 + }), + new Crop("Kale", { + headStart: 4, + maturity: 4, + species: ["brassica napus", "brassica oleracea"], + springPlant: -4, + transplant: true + }), + new Crop("Lettuce (Leaf/Mini Head)", { + headStart: 4, + interval: 2, + maturity: 7, + pause: 3, + resume: 11, + spacing: 4, + species: ["latuca sativa"], + springPlant: -4, + transplant: true + }), + new Crop("Mache (Corn Salad)", { + interval: 2, + maturity: 7, + pause: 1, + resume: 11, + spacing: 16, + species: ["valerianella locusta"], + springPlant: -4 + }), + new Crop("Mustard (Asian Greens)", { + headStart: 3, + interval: 2, + maturity: 6, + pause: 3, + resume: 10, + spacing: 4, + species: ["brassica juncea", "brassica rapa"], + springPlant: -4, + transplant: true + }), + new Crop("Onion", { + headStart: 6, + maturity: 14, + multisow: 3, + spacing: 4, + springPlant: -4, + transplant: true + }), + new Crop("Parsley", { + headStart: 6, + maturity: 7, + spacing: 4, + species: ["petroselinum crispum"], + springPlant: -4, + transplant: true + }), + new Crop("Parsnip", { + maturity: 14, + spacing: 4, + species: ["pastinaca sativa"], + springPlant: -2 + }), + new Crop("Pea", { + fallCrop: 'sow', + maturity: 12, + spacing: 8, + species: ["pisum sativum"], + springPlant: -4, + trellis: true + }), + new Crop("Pepper", { + headStart: 8, + maturity: 10, + species: ["capsicum annuum"], + springPlant: 3, + transplant: true + }), + new Crop("Potato", { + maturity: 12, + spacing: 4, + species: ["solanum tuberosum"], + springPlant: -3 + }), + new Crop("Radish (Round)", { + interval: 2, + maturity: 4, + multisow: 4, + pause: 6, + resume: 10, + spacing: 4, + species: ["raphanus raphanistrum"], + springPlant: -3 + }), + new Crop("Radish (Daikon)", { + fallCrop: true, + maturity: 9, + spacing: 4, + species: ["raphanus raphanistrum"] + }), + new Crop("Shallot", { + headStart: 6, + maturity: 9, + multisow: 3, + spacing: 4, + species: ["allium cepa"], + springPlant: -4, + transplant: true + }), + new Crop("Spinach", { + interval: 2, + maturity: 6, + multisow: 4, + pause: -1, + resume: 11, + spacing: 4, + species: ["spinacia olerace"], + springPlant: -5 + }), + new Crop("Squash (Summer)", { + maturity: 8, + spacing: 1.0 / 9, + species: ["cucurbita pepo"], + springPlant: 0 + }), + new Crop("Squash (Winter)", { + maturity: 12, + spacing: 0.5, + species: ["cucurbita maxima", "cucurbita moschata", "cucurbita pepo"], + springPlant: 1 + }), + new Crop("Swiss Chard", { + headStart: 4, + maturity: 4, + spacing: 4, + species: ["beta vulgaris"], + springPlant: -3, + transplant: true + }), + new Crop("Tomato (Indeterminate)", { + headStart: 7, + maturity: 11, + species: ["lycopersicon esculentum"], + springPlant: 3, + transplant: true, + trellis: true + }), + new Crop("Turnip (Salad)", { + interval: 2, + maturity: 5, + multisow: 4, + pause: 6, + resume: 10, + spacing: 16, + species: ["brassica rapa"], + springPlant: -3 + }) +]; + +function makeCropSchedule(crop, season) { function isSuccession(offset) { + const start = offset <= crop.pause ? crop.springPlant : crop.resume; 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; + offset <= fallPlant && + (offset <= crop.pause || + offset >= season.length - crop.resume) && + (offset - start) % 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; - } + const springStart = crop.springPlant - crop.headStart; + const fallPlant = season.length - crop.maturity - FALL_FACTOR; + const fallStart = fallPlant - crop.headStart; - function sow() { - return icon('direct-sow'); - } + return season.weeks.map(week => { + function action(type) { + const date = weeksLater(season.lastFrostDate, week.offset); + return { + date: date, + actions: type ? [type] : [] + }; + } + + if(crop.transplant && week.offset == springStart) { + return action("start"); + } else if(crop.transplant && week.offset == crop.springPlant) { + return action("transplant"); + } else if(week.offset == crop.springPlant) { + return action("sow"); + } else if(isSuccession(week.offset)) { + return action("sow"); + } else if(crop.fallCrop && crop.transplant && week.offset == fallStart) { + return action("start"); + } else if(crop.fallCrop && crop.transplant && week.offset == fallPlant) { + return action("transplant"); + } else if(crop.fallCrop && week.offset == fallPlant) { + return action("sow"); + } - function start() { - return icon('start-indoors'); + return action(null); + }); +} + +function clearElement(element) { + while (element.firstChild) { + element.removeChild(element.firstChild); } +} - function transplant() { - return icon('transplant'); +function makeSchedule(crops, season) { + const schedule = {}; + crops.forEach(crop => { + schedule[crop.name] = makeCropSchedule(crop, season); + }); + return schedule; +} + +function monthAndDay(date) { + return (date.getUTCMonth() + 1) + "/" + date.getUTCDate(); +} + +function makeDetailLink(crop) { + const span = document.createElement("span"); + span.classList.add("crop"); + span.appendChild(document.createTextNode(crop.name)); + span.addEventListener("click", event => { + viewCropDetails(crop); + }); + return span; +} + +function refreshTable(crops, season, schedule) { + function createWeeksRow(season) { + const row = document.createElement("tr"); + row.appendChild(document.createElement("th")); + season.weeks.forEach(week => { + const td = document.createElement("th", {scope: "col"}); + const firstFrost = week.offset == 0; + const lastFrost = week.offset == season.length; + const dateString = monthAndDay(week.date); + const label = firstFrost || lastFrost ? `${dateString}❄`: dateString; + const text = document.createTextNode(label); + td.appendChild(text); + row.appendChild(td); + }); + return row; } - function nothing() { - return null; + function createHeader(season) { + const thead = document.createElement("thead"); + const row = document.createElement("tr"); + ["Crop", "Week"].forEach(column => { + const th = document.createElement("th", {scope: "col"}); + const text = document.createTextNode(column); + th.appendChild(text); + row.appendChild(th); + }); + thead.appendChild(row); + thead.appendChild(createWeeksRow(season)); + return thead; } - 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()}; + function createCropRow(crop, schedule) { + function icon(name) { + const img = document.createElement("img"); + img.classList.add("icon"); + img.src = `images/${name}.svg`; + return img; } + const row = document.createElement("tr"); + const th = document.createElement("td"); + th.appendChild(makeDetailLink(crop)); + row.appendChild(th); + schedule.forEach(week => { + const td = document.createElement("td"); + td.title = monthAndDay(week.date); + week.actions.forEach(action => { + const iconName = { + start: "start-indoors", + sow: "direct-sow", + transplant: "transplant" + }[action]; + td.appendChild(icon(iconName)); + }); + row.appendChild(td); + }); + return row; + } - 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 scheduleTable = document.getElementById("schedule"); + const tbody = document.createElement("tbody"); + clearElement(scheduleTable); + scheduleTable.appendChild(createHeader(season)); + crops.forEach(crop => { + tbody.appendChild(createCropRow(crop, schedule[crop.name])); }); - 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); + scheduleTable.appendChild(tbody); +} + +function refreshInstructions(crops, season, schedule) { + const instructionsDiv = document.getElementById("instructions"); + season.weeks.forEach(week => { + const actions = crops.flatMap(crop => { + const i = week.offset + WEEKS_BEFORE_LAST_FROST; + return schedule[crop.name][i].actions.map(action => { + return { + crop: crop, + type: action + }; + }); + }).sort((a, b) => { + if(a.type < b.type) { + return -1; + } else if(a.type > b.type) { + return 1; + } + + return 0; + });; + + if(actions.length > 0) { + const weekDiv = document.createElement("div"); + const heading = document.createElement("h3"); + heading.appendChild(document.createTextNode(monthAndDay(week.date))); + weekDiv.appendChild(heading); + actions.forEach(action => { + const p = document.createElement("p"); + let text; + if(action.type == "start") { + text = `Start ${action.crop.name} indoors.`; + } else if(action.type == "sow") { + text = `Sow ${action.crop.name} outdoors.`; + } else if(action.type == "transplant") { + text = `Transplant ${action.crop.name} outdoors.`; + } + p.appendChild(document.createTextNode(text)); + weekDiv.appendChild(p); + }); + instructionsDiv.appendChild(weekDiv); } - 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; +function refreshView() { + function loadDate(name) { + const dateString = window.localStorage.getItem(name); + return dateString && new Date(Date.parse(dateString)); + } + + const lastFrostDate = loadDate("lastFrostDate"); + const firstFrostDate = loadDate("firstFrostDate"); + 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); + const season = new Season(lastFrostDate, firstFrostDate); + const schedule = makeSchedule(CROPS, season); + refreshTable(CROPS, season, schedule); + refreshInstructions(CROPS, season, schedule); + } +} + +function viewCropDetails(crop) { + function close() { + container.removeChild(overlay); + } + + function addDetail(name, detail) { + const p = document.createElement("p"); + p.appendChild(document.createTextNode(`${name}: ${detail}`)); + modal.appendChild(p); + } + + const container = document.getElementById("container"); + const overlay = document.createElement("div"); + const modal = document.createElement("div"); + const heading = document.createElement("h2"); + const subheading = document.createElement("h4"); + const speciesDetail = `(${crop.species.join(", ")})`; + heading.appendChild(document.createTextNode(crop.name)); + subheading.appendChild(document.createTextNode(speciesDetail)); + subheading.classList.add("detail-subheading"); + modal.classList.add("modal"); + modal.appendChild(heading); + modal.appendChild(subheading); + if(crop.spacing >= 1) { + addDetail("Spacing", `${crop.spacing} per square foot`); + } else { + addDetail("Spacing", `1 per ${Math.floor(1 / crop.spacing)} square feet`); + } + addDetail("Average time to maturity", + `${crop.maturity} weeks from ${crop.transplant ? "transplant" : "seed"}`); + if(crop.springPlant < 0) { + addDetail("Earliest planting time", + `${Math.abs(crop.springPlant)} weeks before last frost`); + } else if(crop.springPlant > 0) { + addDetail("Earliest planting time", + `${crop.springPlant} weeks after last frost`); + } else { + addDetail("Earliest planting time", "week of last frost"); + } + addDetail("Planting method", crop.transplant ? "Transplant" : "Direct sow"); + if(crop.transplant) { + addDetail("Start indoors", `${crop.headStart} weeks before planting`); + } + if(crop.multisow > 1) { + addDetail("Multisow", `${crop.multisow} seeds per module`); + } else { + addDetail("Multisow", "No"); + } + addDetail("Needs a trellis", crop.trellis ? "Yes" : "No"); + if(crop.interval > 0) { + addDetail("Succession planting interval", `Every ${crop.interval} weeks`); + } + overlay.classList.add("overlay"); + overlay.appendChild(modal); + container.appendChild(overlay); + overlay.addEventListener("click", event => close()); +} + +function getLastFrostDateInput() { + return document.querySelector("input[name='last-frost-date']"); +} + +function getFirstFrostDateInput() { + return document.querySelector("input[name='first-frost-date']"); +} + +function loadConfiguration() { + const lastFrostDate = window.localStorage.getItem("lastFrostDate"); + const firstFrostDate = window.localStorage.getItem("firstFrostDate"); + if(lastFrostDate) { + getLastFrostDateInput().value = lastFrostDate; + } + if(firstFrostDate) { + getFirstFrostDateInput().value = firstFrostDate; } } function loadIntensiveGarden() { - const lastFrostDateInput = document.querySelector("input[name='last-frost-date']"); - lastFrostDateInput.addEventListener('input', event => { - refreshTable(); + loadConfiguration(); + getLastFrostDateInput().addEventListener("input", event => { + window.localStorage.setItem("lastFrostDate", event.target.value); + refreshView(); + }); + getFirstFrostDateInput().addEventListener("input", event => { + window.localStorage.setItem("firstFrostDate", event.target.value); + refreshView(); }); - refreshTable(); + refreshView(); } -window.addEventListener('load', loadIntensiveGarden); +window.addEventListener("load", loadIntensiveGarden); @@ -6,7 +6,7 @@ <link href="css/garden.css" rel="stylesheet"> </head> <body> - <div class="container"> + <div id="container" class="container"> <h1>Intensive Vegetable Garden Scheduler</h1> <p> This handy tool helps plan your intensively planted, organic, @@ -35,17 +35,17 @@ </p> <p> Finally, this planner is intended for gardeners located in the - nothern areas of the northern hemisphere where we need to - start many summer crops indoors and cannot harvest during - winter without season extension. All planting dates are made - under the assumption that no season extension techniques are - being applied. + nothern areas of the northern hemisphere (USDA zone 7 and below) + where we need to start many summer crops indoors and cannot + harvest during winter without season extension. All planting + dates are made under the assumption that no season extension + techniques are being applied. </p> <p> <a href="https://git.dthompson.us/intensive-garden-planner.git">Source code</a> is available under the GNU Affero General Public - License version 3 or later. Perhaps you'd like to make an - improvement or modify it for use in the southern hemisphere? + License version 3 or later. Perhaps you'd like to add a new + crop or fix a bug? </p> <h2>Configuration</h2> <div> @@ -56,14 +56,18 @@ <label for="first-frost-date">First frost date:</label> <input name="first-frost-date" type="date"></input> </div> - <h2>Key</h2> - <ul> - <li><img src="images/start-indoors.svg" class="key"/>: start seeds indoors</li> - <li><img src="images/direct-sow.svg" class="key"/>: direct sow outdoors</li> - <li><img src="images/transplant.svg" class="key"/>: transplant outdoors</li> - </ul> <h2>Schedule</h2> - <table id="schedule"></table> + <div> + <h3>Key</h3> + <ul> + <li><img src="images/start-indoors.svg" class="key"/>: start seeds indoors</li> + <li><img src="images/direct-sow.svg" class="key"/>: direct sow outdoors</li> + <li><img src="images/transplant.svg" class="key"/>: transplant outdoors</li> + </ul> + <table id="schedule"></table> + </div> + <h2>Week-by-week instructions</h2> + <div id="instructions"></div> </div> </body> </html> |