summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson@vistahigherlearning.com>2022-01-22 07:46:23 -0500
committerDavid Thompson <dthompson@vistahigherlearning.com>2022-01-22 07:46:23 -0500
commit592bc33aa3c03e15d0deccfe8dcfd4188da2015d (patch)
tree1634044148a81a9886bab153d10fafabd976c575
parentbf0d6bc9600d2a5059e1a23add5eb64c11f0fa6e (diff)
Separate view and data layer, add more crop details, simplify Crop class.
-rw-r--r--css/garden.css52
-rw-r--r--garden.js815
-rw-r--r--index.html34
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%;
+}
diff --git a/garden.js b/garden.js
index f1e6064..8e27c31 100644
--- a/garden.js
+++ b/garden.js
@@ -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);
diff --git a/index.html b/index.html
index 0155fc5..5c49e66 100644
--- a/index.html
+++ b/index.html
@@ -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>