Foraging with Org

Posted on December 21, 2023

Probably three years ago now I got into the hobby of foraging for fungi. I've managed to find chicken of the woods, puffballs, a couple varieties of oyster, and most recently wood ear in my area. I've heard whispers of the coveted morel, but I have yet to find any, much to my chagrin. Anyway, after rooting around for mushrooms for a while, I decided I would like to forage for more things than just fungus, and I decided to play with Org to see if I could come up with a fun way to keep track of it all. it turns out, fungi are complicated enough, so we'll stick to that for the purposes of this post.

The first thing I built was a field guide, which looks something like this:

* Field Guide

** Black Trumpets
:PROPERTIES:
:REF:      [[https://ediblewildmushrooms.com/black-trumpets.htm]]
:Season:   [06-01]--[09-30]
:END:

#+ATTR_LATEX: :width 5cm :height 5cm
[[./images/foraging/b-trump1.jpg]]

#+begin_quote
The fragrant and often abundant Black Chanterelle or Black Trumpet (Craterellus
fallax) is a popular mushroom in French cuisine because of it's unique flavor
and texture.  Fragrant fresh specimens are said to resemble apricots.  This
summer mushroom is relatively easy to find and safe to identify.  Black Trumpets
are found in mixed deciduous woods, mostly under Oaks, on the ground.  This
mushroom does not typically grow on wood.  The stalk is an extension of the
trumpet shaped cap.  Coloring of the spore surface and stem can range from
salmon to pale grey to nearly black with wrinkles or raised veins rather than
gills..  The cap and interior is usually darker, salmon brown to black and
hollow top to bottom.  They usually appear anywhere from June to September.
#+end_quote

Each entry is created using an Org-Capture template that takes a reference to the site where I found the information on the mushroom I want to forage, the season I can expect to find it, a picture of said mushroom, and a quote from the reference describing it. Foraging is an inherently risky hobby, though, and each entry really deserves more than just a few sentences taken out of context from the internet. The point of the field guide entry is to help get positive IDs on the things I find, and there are many questions to answer about a mushroom before an ID should be considered positive: Does the mushroom have pores, gills or false gills? Are the gills confined to the cap or do they go down the stipe? How far? Do the gills remain separate over their entire length or do they converge? What color is the spore print? Does the mushroom have a distinct smell? Are there variations in cap color? What are the lookalikes that are in my area that I should be concerned about? Are the lookalikes poisonous?

This is a lot of information, and although a picture is worth a thousand words, and in conjunction with smell gets me a positive ID most of the time (my area has surprisingly few lookalikes for the things I forage) it would be ideal to lay this information out in a more structured way. This sounds like it should be straightforward, but when we try to expand the scope of this information set just a little bit by looking at fungus other than mushrooms, we run into a glaring problem. Look at a jelly fungus like Wood Ear: it doesn't have gills, stipes, or caps. Getting a positive ID for Wood Ear involves a very different set of questions than those used to identify a mushroom. Unfortunately, I don't have a solution for this problem yet, but I'm hoping future me can figure it out, so I'll move on for now.

As I read in books or online about new things that I am interested in trying to find, I create entries for them in the Field Guide section of my foraging file. Books are simple, because there's really only one thing for it (outside of an OCR solution): I create an entry in my foraging guide with an Org-Capture template and manually copy the salient descriptions that get me a positive ID of the thing in question. With online sources, the only extra question I had to answer was whether or not to use org-protocol to facilitate using a browser to fill out my Org-Capture template for me1. This was actually a difficult decision since it would involve returning to running Emacs as a daemon. Since I close and reopen Emacs all the time due to botched on-the-fly config changes, I decided Org-Protocol wasn't worth it. Capturing information from online sources is more manual as a result, but it's not one I use too often so it's fine.

After getting a field guide together, the next step was keeping a journal of what I found and where. This turned out to be surprisingly easy to accomplish with a single line in my org file:

#+TODO: TRACK FOUND(w@)

This line sets the todo states to TRACK and FOUND. The super neat thing is the (w@), which tells Emacs to run org-add-note whenever the headline's state is changed to whatever the (w@) is attached to. This function creates an ephemeral buffer with a configurable note template which by default includes the state transitions and the date the transition took place. You just fill out whatever additional context you want in the note, hit C-c C-c, and you're done. Here's an example of the notes that I take:

- State "FOUND"      from "TRACK"      [2021-10-02 Sat 15:44] \\
Random Sate Park, at the intersection of Main Trail, and Backwoods Trail, around
90 paces north on Backwoods

So whenever I want to go foraging, I set the states for what I expect I might find to TRACK, and whenever I find something I change its state to FOUND which adds a new note. Alternatively, the TRACK state could be omitted entirely, but then the first line of the note would read - State "FOUND" from [2021-10-02 Sat 15:44] \\, and I don't want to put in the tweaking to make the from go away when there's no previous state, so I'm leaving the TRACK state in. Anyway, this journal feature alone is enough to call this endeavor a success, but since I now have dates tied to when I find things, wouldn't it be great to pull all of this information together into a visual representation? Of course it would! The code will be pretty difficult to follow, so to provide some context, here's what the end result looks like:

The picture is more of a Gantt chart than a calendar or a schedule, but there's no way I'm going to call it that. What you're seeing is a primitive plot for every entry in my field guide. The code pulls the season from the entry's properties and creates the light blue box representing when I might find something in my field guide. Each magenta line corresponds to a date extracted from a en entry's log. This requires me to be diligent about logging my findings, which I'm usually excited to do. Running the code prompts me for a day I expect to be foraging, which is represented in the plot by the green line. Any entry whose season overlaps that green line is listed in the "Today's Forageables" heading. I extended Wood Ear's season to the end of December to demonstrate this. Wood Ear wouldn't normally be around this time of year, but it's been unseasonably warm to the point where I wouldn't be surprised if I managed to track some down. You can also get an idea of what the field guide entry I described earlier actually looks like here.

If you couldn't tell by the LaTeX metadata I added to the picture there, this file was designed to be exported to a PDF in order to be more accessible from a mobile device. Otherwise, I'd change the color of the SVG to match my Gruvbox theme. As for where it goes, I put it on my WebDAV server and pull it up on my phone over VPN when I'm out and about to figure out where I'm going and what I might find there.

I could have made a package for this, but I decided to just use a source block in my foraging file. Accomplishing this turned out to be more complex than I anticipated, and it's still one big messy function. As soon as I figure out a nice way to declare functions I don't want to leave lying around (puttihg them in let bindings always looks strange to me), I promise I'll refactor it. For now, I'm hoping the picture above and the comments in the code give you enough context and explanation to figure out what's going on. Without further ado:

(save-excursion
  (let ((forageables)
        (today (org-read-date)))
    ;; Gather data to create visual for what's been foraged this year, and a
    ;; list of what can be foraged today
    (goto-char (org-find-exact-headline-in-buffer "Field Guide"))
    (org-map-entries (lambda()
                       (let* ((element (nth 1 (org-element-at-point)))
                              (season)
                              (logs))
                         (when (re-search-forward ":LOGBOOK:"
                                                  (save-excursion
                                                    (outline-next-heading)
                                                    (point))
                                                  t)
                           (let* ((elt (org-element-property-drawer-parser nil))
                                  (beg (org-element-property :contents-begin elt))
                                  (end (org-element-property :contents-end elt)))
                             ;; Parse logs into dates when a forageable was found
                             (setq logs (when (and beg end)
                                          (remove nil (mapcar (lambda (item)
                                                                (when (string-match "\\[[0-9]\\{4\\}\-\\([0-9]\\{2\\}\-[0-9]\\{2\\}\\)\s[A-Z][a-z]\\{2\\}\\(\s[0-9]\\{2\\}:[0-9]\\{2\\}\\)\\]" item)
                                                                  (string-trim (match-string 1 item))))
                                                              (split-string (buffer-substring-no-properties beg end)
                                                                            "\n"
                                                                            t)))))))
                         (when (and (stringp (plist-get element :title))
                                    (not (string= (plist-get element :title) "Field Guide")))
                           (push `(:NAME ,(plist-get element :title)
                                         :SEASON ,(split-string (plist-get element :SEASON)
                                                                "--"
                                                                t
                                                                "\\(\\[\\|\\]\\)")
                                         :LOGS ,logs)
                                 forageables))))
                     nil
                     'tree)
    (setq forageables (reverse forageables))
    ;; Create a list of forageables that are in season
    (goto-char (org-find-exact-headline-in-buffer "Today's Forageables"))
    (org-cut-subtree)
    (insert (concat "* Today's Forageables\n\n"
                    (mapconcat (lambda (item)
                                 (format "- [[*%1$s][%1$s]]" (plist-get item :NAME)))
                               ;; Filter out forageables that are out of season
                               (seq-filter (lambda (item)
                                             (let ((today-month (substring today 5 nil))
                                                   (start (nth 0 (plist-get item :SEASON)))
                                                   (end (nth 1 (plist-get item :SEASON))))
                                               (and (string> end today-month) (string> today-month start))))
                                           forageables)
                               "\n")
                    "\n\n"))
    ;; Generate a foraging schedule as an SVG
    (let* ((colW 42)
           (colH 24)
           (itemW 160)
           (font-size 14)
           (item-ct (length forageables))
           (W (+ itemW (* colW 12)))
           (H (* colH (+ item-ct 1)))
           (months '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))
           (days '(31 28 31 30 31 30 31 31 30 31 30 31))
           (ctr 0)
           ;; Create SVG
           (svg (svg-create W
                            H
                            :stroke "black"
                            :stroke-width 1
                            :font-family "Alegreya")))
      ;; Draw rectangle around whole figure
      (svg-rectangle svg
                     0
                     0
                     W
                     H
                     :stroke-width 1
                     :stroke-color "black"
                     :fill-color "white")
      ;; Draw date
      (svg-text svg
                today
                :x (/ itemW 2)
                :y (- colH 7)
                :font-size font-size
                :font-family "Alegreya"
                :fill "black"
                :text-anchor "middle")
      ;; Draw box around date
      (svg-rectangle svg
                     0
                     0
                     itemW
                     colH
                     :stroke-width 1
                     :stroke-color "black"
                     :fill-opacity 0)
      (dolist (month months)
        ;; Draw month text
        (svg-text svg (nth ctr months) :x (+ itemW (* (+ ctr 0.5) colW)) :y (- colH 7) :font-size font-size :fill "black" :text-anchor "middle")
        ;; Draw box around month text
        (svg-rectangle svg
                       (+ itemW (* ctr colW))
                       0
                       colW
                       colH
                       :stroke-width 1
                       :stroke-color "black"
                       :fill-opacity 0)
        (setq ctr (+ ctr 1)))
      (setq ctr 0)
      (dolist (forageable forageables)
        (let ((start (split-string (nth 0 (plist-get forageable :SEASON)) "-"))
              (end (split-string (nth 1 (plist-get forageable :SEASON)) "-")))
          (setq ctr (+ ctr 1))
          ;; Draw forageable name
          (svg-text svg
                    (plist-get forageable :NAME)
                    :x (/ itemW 2)
                    :y (- (* (+ ctr 1) colH) 7)
                    :font-family "Alegreya"
                    :font-size font-size
                    :fill "black"
                    :text-anchor "middle")
          ;; Draw box around name
          (svg-rectangle svg
                         0
                         (* ctr colH)
                         itemW
                         colH
                         :stroke-width 1
                         :stroke-color "black"
                         :fill-opacity 0)
          ;; Draw box representing the season the forageable can be found
          (svg-line svg
                    (+ itemW (* colW (- (string-to-number (nth 0 start)) 1))
                       (* colW (/ (float (string-to-number (nth 1 start)))
                                  (nth (- (string-to-number (nth 0 start)) 1) days)))
                       1)
                    (* (+ ctr 0.5) colH)
                    (+ itemW (* colW (- (string-to-number (nth 0 end)) 1))
                       (* colW (/ (float (string-to-number (nth 1 end)))
                                  (nth (- (string-to-number (nth 0 end)) 1) days))))
                    (* (+ ctr 0.5) colH)
                    :stroke-color "skyblue"
                    :stroke-width (- colH 4))
          (dolist (log (plist-get forageable :logs))
            (let* ((event (split-string log "-"))
                   (eventW (+ itemW (* colW (- (string-to-number (nth 0 event)) 1))
                              (* colW (/ (float (string-to-number (nth 1 event)))
                                         (nth (- (string-to-number (nth 0 event)) 1) days))))))
              ;; Draw marker representing each date a forageable was found
              (svg-line svg
                        eventW
                        (* ctr colH)
                        eventW
                        (* (+ ctr 1) colH)
                        :stroke-color "magenta"
                        :stroke-width 2)))))
      (let* ((today-split (split-string today "-"))
             (todayW (+ itemW
                        (* colW (- (string-to-number (nth 1 today-split)) 1))
                        (* colW (/ (float (string-to-number (nth 2 today-split)))
                                   (nth (- (string-to-number (nth 1 today-split)) 1) days))))))
        ;; Draw line representing today
        (svg-line svg
                  todayW
                  colH
                  todayW
                  H
                  :stroke-color "limegreen"
                  :stroke-width 2))
      ;; Write the SVG to a file
      (with-temp-file "images/foraging/schedule.svg"
        (set-buffer-multibyte nil)
        (svg-print svg))
      ;; Convert SVG to PNG so it can be exported to PDF
      (shell-command "inkscape --export-type=png images/foraging/schedule.svg")
      (delete-file "images/foraging/schedule.svg"))))
(org-redisplay-inline-images)

I'm sure this code is overwhelming to look at, but the majority of it is drawing the SVG. It was a lot of code for what looks like a small payoff, but seeing it through felt pretty great and it was sort of fun to learn how to build an SVG the Emacs way. Also, I'm pretty proud of how it turned out. That's it for now!

Footnotes

1"org-protocol.el - intercept calls from emacsclient to trigger custom actions", orgmode.org, accessed December 19, 2023. https://orgmode.org/worg/org-contrib/org-protocol.html.