;;; xtide.el --- XTide display in Emacs.

;; Copyright (C) 2006 Kevin Ryde
;;
;; xtide.el is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 2, or (at your option) any later
;; version.
;;
;; xtide.el is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
;; more details.
;;
;; You can get a copy of the GNU General Public License online at
;; http://www.gnu.org/licenses/gpl.txt, or you should have one in the file
;; COPYING which comes with GNU Emacs and other GNU programs.  Failing that,
;; write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
;; Boston, MA 02110-1301 USA.

;;; Commentary:

;; M-x xtide displays a tidal graph in Emacs using the "tide" program from
;; XTide (http://www.flaterco.com/xtide).
;;
;; M-x calendar is extended with a "T" key to display the tides for the
;; selected day.
;;
;; The graph is shown as a PNG image if the display supports that, otherwise
;; XTide's ascii-art fallback.  Left and right arrow keys move forward or
;; back in time.  The various alterative displays from xtide, like "p" plain
;; times, can be viewed too (use C-h m to see the possibilities).
;;

;;; Install:

;; To make M-x xtide available, put xtide.el somewhere in your load-path and
;; the following in your .emacs
;;
;;     (autoload 'xtide "xtide" nil t)
;;
;; To get the calendar mode "T" key bound when calendar loads, add
;;
;;     (autoload 'xtide-calendar-setups "xtide.el" nil t)
;;     (add-hook 'calendar-load-hook 'xtide-calendar-setups)
;;
;; Don't forget you need to set an XTIDE_DEFAULT_LOCATION environment
;; variable, usually in your .profile.  You can also do it in .emacs if you
;; want, for instance
;;
;;     (setenv "XTIDE_DEFAULT_LOCATION" "Warrnambool")
;;

;;; History:

;; Version 1 - the first version.

;;; Code:

(defgroup xtide nil
  "Xtide."
  :prefix "xtide-"
  :group 'applications)

(defcustom xtide-watchlist-hook nil
  "*Hook called by `xtide-mode'."
  :type  'hook
  :group 'xtide)

(defconst xtide-buffer "*xtide*")

(defun xtide-run (time mode f-option)
  "Run xtide on TIME, MODE and F-OPTION.
TIME is a list in `current-time' style.
MODE is a -m option string, like \"g\" for graph.
F-OPTION is \"-ft\" for text, or \"-fp\" for png.
The output from xtide is inserted into the current buffer and the return
value is 0 for success, or a string describing an error.  The string
includes anything xtide printed to stderr."
  (let ((status    nil)
        (old-point (point))
        (tempfile  (make-temp-file "xtide-el-")))
    (unwind-protect
        (progn
          ;; in emacs 21 with-temp-message doesn't clear a message when it's
          ;; done, so don't use that
          (message "Running xtide...")

          ;; most output is short enough that redisplay doesn't make a
          ;; difference, but the "-ml" listing is long and redisplay on it
          ;; is bad, there's no point displaying when we're going to go back
          ;; to the start of the buffer anyway, hence "nil" redisplay
          ;; parameter
          ;;
          (setq status (call-process "tide" nil (list t tempfile) nil
                                     f-option
                                     "-m" mode
                                     "-b" (format-time-string "%Y-%m-%d %H:%M"
                                                              time)))
          ;; when the location is not found the exit status is still 0, but
          ;; there's nothing written to stdout
          (unless (and (eq 0 status)
                       (/= (point) old-point))
            (setq status
                  (concat (with-temp-buffer
                            (insert-file-contents tempfile)
                            (buffer-string))
                          "\n"
                          (if (stringp status)
                              status
                            (format "Exit %s" status))))))
      (message nil)
      (delete-file tempfile))
    status))

(defun xtide-insert-image (time)
  "Insert a tide display graph in PNG image form in the current buffer.
TIME is in `current-time' format.
If there's an error running xtide, the error message is inserted in the
buffer instead."
  (let* ((status nil)
         (image  (with-temp-buffer
                   (set-buffer-multibyte nil)
                   (setq status (xtide-run time "g" "-fp"))
                   `(image :type png :data ,(buffer-string)))))
    (if (eq 0 status)
        (insert-image image)
      (insert status))))

(defun xtide-insert-text (time mode)
  "Insert xtide output in text form in the current buffer.
TIME is in `current-time' format.
MODE is a -m option string, like \"g\" for graph."
  (let* ((status (xtide-run time mode "-ft")))
    (unless (eq 0 status)
      (insert status))))

(defun xtide-forward-6hour ()
  "Go forward 6 hours in the tidal display."
  (interactive)
  (require 'time-date)
  ;; `time-add' is only in emacs 22, so go through floating point seconds
  (xtide-mode (seconds-to-time (+ (time-to-seconds xtide-time) 21600))
              xtide-output-mode))

(defun xtide-backward-6hour ()
  "Go back 6 hours in the tidal display."
  (interactive)
  (require 'time-date)
  (xtide-mode (subtract-time xtide-time '(0 21600 0)) xtide-output-mode))

(defun xtide-change-mode ()
  "Change xtide mode (to the key that invoked this command).
The modes are:
    a - about the location
    b - banner style vertical graph (text only)
    c - calendar of tide times
    C - alternate calendar format
    g - graph of tide height
    l - list locations available
    m - medium rare times, heights in raw-ish format
    p - plain times of tides, moonrise and sunrise
    r - raw times and heights
    s - statistics for the location"
  (interactive)
  (xtide-mode xtide-time (this-command-keys)))

(defvar xtide-mode-map
  (let ((m (make-sparse-keymap)))
    (define-key m "q"     'kill-this-buffer)
    (define-key m "s"     'xtide-change-mode)
    (define-key m "r"     'xtide-change-mode)
    (define-key m "p"     'xtide-change-mode)
    (define-key m "m"     'xtide-change-mode)
    (define-key m "l"     'xtide-change-mode)
    (define-key m "g"     'xtide-change-mode)
    (define-key m "C"     'xtide-change-mode)
    (define-key m "c"     'xtide-change-mode)
    (define-key m "b"     'xtide-change-mode)
    (define-key m "a"     'xtide-change-mode)
    (define-key m " "     'xtide-forward-6hour)
    (define-key m [?\d]   'xtide-backward-6hour)
    (define-key m [left]  'xtide-backward-6hour)
    (define-key m [right] 'xtide-forward-6hour)
    m)
  "Keymap for `xtide-mode' display buffer.")

(defun xtide-mode (&optional time mode)
  "Mode for xtide display.

\\{xtide-mode-map}"

  (unless time
    (setq time (current-time)))
  (unless mode
    (setq mode "g"))
  (setq buffer-read-only nil)
  (erase-buffer)
  (kill-all-local-variables)
  (use-local-map xtide-mode-map)
  (setq major-mode 'xtide-mode
        mode-name  "XTide"
        truncate-lines t)

  ;; save parameters, for future scrolling
  (set (make-local-variable 'xtide-time)        time)
  (set (make-local-variable 'xtide-output-mode) mode)

  (if (and (string-equal mode "g")
           (display-images-p)
           (image-type-available-p 'png))
      (xtide-insert-image time)
    (xtide-insert-text time mode))

  (goto-char (point-min))
  (set-buffer-modified-p nil)
  (setq buffer-read-only t)

  (run-hooks 'xtide-mode-hook))
  
;;;###autoload
(defun xtide ()
  "Display a tidal graph using XTide."
  (interactive)
  (switch-to-buffer (get-buffer-create xtide-buffer))
  (if (getenv "XTIDE_DEFAULT_LOCATION")
      (xtide-mode nil "g")
    (xtide-mode nil "l")
    (message "No XTIDE_DEFAULT_LOCATION set, showing list of locations")))

(defun xtide-calendar-tides (&optional arg)
  "Display tides for the cursor date.
With a prefix ARG, prompt for a date to display."
  (interactive "P")
  (let ((date (if arg
                  (calendar-read-date t)
                (calendar-cursor-to-date t))))
    (display-buffer (get-buffer-create xtide-buffer))
    (with-current-buffer xtide-buffer
      ;; xtide shows from a time a little before what's asked for, so give
      ;; 2:30am to get about 1:00am
      (xtide-mode (encode-time 0 30 2
                               (extract-calendar-day   date)
                               (extract-calendar-month date)
                               (extract-calendar-year  date))))))

;;;###autoload
(defun xtide-calendar-setups ()
  "Setup xtide additions to `calendar-mode'."
  (define-key calendar-mode-map "T" 'xtide-calendar-tides))

;; do it now if calendar is loaded, otherwise setup for when its ready
(if (boundp 'calendar-mode-map)
    (xtide-calendar-setups))
(add-hook 'calendar-load-hook 'xtide-calendar-setups)


(provide 'xtide)

;;; xtide.el ends here
