emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] 01/02: Add metar.el.


From: Mario Lang
Subject: [elpa] 01/02: Add metar.el.
Date: Mon, 26 May 2014 12:56:45 +0000

mlang pushed a commit to branch master
in repository elpa.

commit 8aadf0c0d68e6cbff0fca08dc78d86eea642bbb8
Author: Mario Lang <address@hidden>
Date:   Sun May 25 00:02:45 2014 +0200

    Add metar.el.
---
 packages/metar/metar.el |  508 +++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 508 insertions(+), 0 deletions(-)

diff --git a/packages/metar/metar.el b/packages/metar/metar.el
new file mode 100644
index 0000000..3a04b47
--- /dev/null
+++ b/packages/metar/metar.el
@@ -0,0 +1,508 @@
+;;; metar.el --- Retrieve and decode METAR weather information
+
+;; Copyright (C) 2007, 2014  Free Software Foundation, Inc.
+
+;; Author: Mario Lang <address@hidden>
+;; Version: 0
+;; Package-Requires: ((cl-lib "0.5"))
+;; Keywords: comm
+
+;; This program 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 3 of the License, or
+;; (at your option) any later version.
+
+;; This program 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 should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Run `M-x metar RET' to get a simple weather report from weather.noaa.gov.
+;; The value of `calendar-latitude' and `calendar-longitude' will be used to
+;; automatically determine a nearby station.  If these variables are not set,
+;; you will be prompted to enter the location manually.
+;;
+;; With `C-u M-x metar RET', country and station name need to be entered.
+;; `C-u C-u M-x metar RET' will prompt for the METAR station code (4 letters).
+;;
+;; For programmatic access to decoded weather reports, use:
+;;
+;;   (metar-decode (metar-get-record "CODE"))
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'rx)
+(require 'solar)
+(require 'url)
+
+(defvar metar-stations-info-url "http://weather.noaa.gov/data/nsd_bbsss.txt";
+  "URL to use for retrieving station meta information.")
+
+(defvar metar-stations nil
+  "Variable containing (cached) METAR station information.
+Use the function `metar-stations' to get the actual station list.")
+
+(defun metar-stations ()
+  "Retrieve a list of METAR stations.
+Results are cached in variable `metar-stations'.
+If this variable is nil, the information is retrieved from the Internet."
+  (or metar-stations
+      (let ((data (with-temp-buffer
+                   (url-insert-file-contents metar-stations-info-url)
+                   (mapcar (lambda (entry)
+                             (split-string entry ";"))
+                           (split-string (buffer-string) "\n")))))
+       (setq metar-stations nil)
+       (while data
+         (when (and (nth 7 (car data)) (nth 8 (car data))
+                    (not (string= (nth 2 (car data)) "----")))
+           (setq metar-stations
+                 (append
+                  (let ((item (car data)))
+                    (list
+                     (list (cons 'code (nth 2 item))
+                           (cons 'name (nth 3 item))
+                           (cons 'country (nth 5 item))
+                           (cons 'latitude
+                                 (when (string-match 
"^\\([0-9]+\\)-\\([0-9]+\\)\\(-[0-9]+\\)?\\([NS]\\)" (nth 7 item))
+                                     (funcall (if (string= (match-string 4 
(nth 7 item)) "N") #'+ #'-)
+                                              (+ (string-to-number 
(match-string 1 (nth 7 item)))
+                                                 (/ (string-to-number 
(match-string 2 (nth 7 item)))
+                                                    60.0)))))
+                           (cons 'longitude
+                                 (when (string-match 
"^\\([0-9]+\\)-\\([0-9]+\\)\\(-[0-9]+\\)?\\([WE]\\)" (nth 8 item))
+                                   (funcall (if (string= (match-string 4 (nth 
8 item)) "E") #'+ #'-)
+                                            (+ (string-to-number (match-string 
1 (nth 8 item)))
+                                               (/ (string-to-number 
(match-string 2 (nth 8 item)))
+                                                  60.0)))))
+                           (cons 'altitude (string-to-number (nth 12 item))))))
+                  metar-stations)))
+         (setq data (cdr data)))
+       ;; (unless metar-timer
+       ;;   (setq metar-timer
+       ;;      (run-with-timer 600 nil (lambda () (setq metar-stations nil)))))
+       metar-stations)))
+
+(defun metar-stations-get (station-code key)
+  "Get meta information for station with STATION-CODE and KEY.
+KEY can be one of the symbols `code', `name', `country', `latitude',
+`longitude' or `altitude'."
+  (let ((stations (metar-stations)) result)
+    (while stations
+      (when (string= (cdr (assoc 'code (car stations))) station-code)
+       (setq result (cdr (assoc key (car stations)))
+             stations nil))
+      (setq stations (cdr stations)))
+    result))
+
+(defun metar-latitude-longitude-bearing (latitude1 longitude1 latitude2 
longitude2)
+  "Calculate bearing from start point LATITUDE1/LONGITUDE1 to end point
+LATITUDE2/LONGITUDE2."
+  (% (+ 360
+       (truncate
+        (radians-to-degrees
+         (atan (* (sin (degrees-to-radians (- longitude2 longitude1)))
+                  (cos (degrees-to-radians latitude2))) 
+               (- (* (cos (degrees-to-radians latitude1))
+                     (sin (degrees-to-radians latitude2)))
+                  (* (sin (degrees-to-radians latitude1))
+                     (cos (degrees-to-radians latitude2))
+                     (cos (degrees-to-radians (- longitude2 longitude1)))))))))
+     360))
+
+(defun metar-latitude-longitude-distance-haversine (latitude1 longitude1
+                                             latitude2 longitude2)
+  "Caluclate the distance (in kilometers) between two points on the
+surface of the earth given as LATITUDE1, LONGITUDE1, LATITUDE2 and LONGITUDE2."
+  (macrolet ((distance (d1 d2)
+              `(expt (sin (/ (degrees-to-radians (- ,d2 ,d1)) 2)) 2)))
+    (let ((a (+ (distance latitude1 latitude2)
+               (* (cos (degrees-to-radians latitude1)) (cos 
(degrees-to-radians latitude2))
+                  (distance longitude1 longitude2)))))
+      (* 6371 (* 2 (atan (sqrt a) (sqrt (- 1 a))))))))
+
+(defun metar-find-station-by-latitude/longitude (latitude longitude &optional
+                                                         radius)
+  "Find a station near the coordinates given by LATITUDE and LONGITUDE.
+Returns a cons where car is the station code and cdr is the distance in
+kilometers.
+If RADIUS is non-nil, only stations within this range (in kilometers) are
+considered.
+If no match if found, nil is returned."
+  (interactive
+   (list
+    (solar-get-number "Enter latitude (decimal fraction; + north, - south): ")
+    (solar-get-number "Enter longitude (decimal fraction; + east, - west): ")))
+  (let ((stations (metar-stations))
+       (best-distance (or radius 10000))
+       (station-code nil))
+    (while stations
+      (let ((station-latitude (cdr (assoc 'latitude (car stations))))
+           (station-longitude (cdr (assoc 'longitude (car stations)))))
+       (when (and station-latitude station-longitude)
+         (let ((distance (metar-latitude-longitude-distance-haversine
+                          latitude longitude
+                          station-latitude station-longitude)))
+           (when (< distance best-distance)
+             (setq best-distance distance
+                   station-code (cdr (assoc 'code (car stations))))))))
+      (setq stations (cdr stations)))
+    (if (called-interactively-p 'interactive)
+       (if station-code
+           (message "%s, %s (%s) at %s is %d km away from %s."
+                    (metar-stations-get station-code 'name)
+                    (metar-stations-get station-code 'country)
+                    station-code
+                    (let ((float-output-format "%.1f"))
+                      (format "%s%s, %s%s"
+                              (abs (metar-stations-get station-code 'latitude))
+                              (if (> (metar-stations-get station-code 
'latitude) 0) "N" "S")
+                              (abs (metar-stations-get station-code 
'longitude))
+                              (if (> (metar-stations-get station-code 
'longitude) 0) "E" "W")))
+                    best-distance
+                    (let ((float-output-format "%.1f"))
+                      (format "%s%s, %s%s"
+                              (if (numberp latitude)
+                                  (abs latitude)
+                                (+ (aref latitude 0)
+                                   (/ (aref latitude 1) 60.0)))
+                              (if (numberp latitude)
+                                  (if (> latitude 0) "N" "S")
+                                (if (equal (aref latitude 2) 'north) "N" "S"))
+                              (if (numberp longitude)
+                                  (abs longitude)
+                                (+ (aref longitude 0)
+                                   (/ (aref longitude 1) 60.0)))
+                              (if (numberp longitude)
+                                  (if (> longitude 0) "E" "W")
+                                (if (equal (aref longitude 2) 'east)
+                                    "E" "W")))))
+         (message "No appropriate station found."))
+      (when station-code
+       (cons station-code (round best-distance))))))
+
+(defun metar-temp-to-number (string)
+  "Convert a METAR temperature to a number."
+  (if (= (aref string 0) ?M)
+      (- (string-to-number (substring string 1)))
+    (string-to-number string)))
+
+(defvar metar-url 
"http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT";
+  "URL used to fetch station specific information.
+%s is replaced with the 4 letter station code.")
+
+(defun metar-url (station)
+  (format metar-url (upcase (cl-etypecase station
+                             (string station)
+                             (symbol (symbol-name station))))))
+
+(defvar metar-record-regexp
+  (rx (group (1+ digit)) ?/ (group (1+ digit)) ?/ (group (1+ digit))
+      space
+      (group (1+ digit)) ?: (group (1+ digit))
+      ?\n
+      (group "%s" (* not-newline)))
+  "Regular expression used to extract METAR information from `metar-url'.
+%s is replaced with the station code which always has to be present in a METAR
+record.")
+
+(defun metar-get-record (station)
+  "Retrieve a METAR/SPECI record for STATION from the Internet.
+REturn a cons where `car' is the time of the measurement (as an emacs-lsip
+time value) and `cdr' is a string containing the actual METAR code.
+If no record was found for STATION, nil is returned."
+  (unless (string-match "^[A-Z][A-Z0-9][A-Z0-9][A-Z0-9]$" station)
+    (signal 'error "Invalid station code"))
+  (with-temp-buffer
+    (url-insert-file-contents (metar-url station))
+    (when (re-search-forward (format metar-record-regexp station) nil t)
+      (cons (encode-time
+            0
+            (string-to-number (match-string 5))
+            (string-to-number (match-string 4))
+            (string-to-number (match-string 3))
+            (string-to-number (match-string 2))
+            (string-to-number (match-string 1))
+            0)
+           (match-string 6)))))
+
+(defconst metar-could-regexp
+  (rx symbol-start
+      (group (or "FEW" "SCT" "BKN" "OVC"))
+      (group (= 3 digit))
+      (optional (group (or "TCU" "CB")))
+      symbol-end))
+
+(defun metar-clouds (info)
+  (let ((clouds ())
+       (from 0))
+    (while (string-match metar-could-regexp info from)
+      (setq from (match-end 0)
+           clouds (push (append (list (match-string 1 info)
+                                      (string-to-number (match-string 2 info)))
+                                (when (match-string 3 info)
+                                  (list (match-string 3 info))))
+                        clouds)))
+    clouds))
+
+(defconst metar-phenomena '(("BC" . "patches")
+                           ("BL" . "blowing")
+                           ("BR" . "mist")
+                           ("DR" . "drifting")
+                           ("DS" . "dust storm")
+                           ("DU" . "widespread dust")
+                           ("DZ" . "drizzle")
+                           ("FC" . "funnel cloud")
+                           ("FG" . "fog")
+                           ("FU" . "smoke")
+                           ("FZ" . "freezing")
+                           ("GR" . "hail")
+                           ("GS" . "small hail/snow pellets")
+                           ("HZ" . "haze")
+                           ("IC" . "ice crystals")
+                           ("MI" . "shallow")
+                           ("PL" . "ice pellets")
+                           ("PO" . "well developed dust/sand swirls")
+                           ("PR" . "partials")
+                           ("PY" . "spray")
+                           ("RA" . "rain")
+                           ("SA" . "sand")
+                           ("SG" . "snow grains")
+                           ("SH" . "showers")
+                           ("SN" . "snow")
+                           ("SQ" . "squall")
+                           ("SS" . "sand storm")
+                           ("TS" . "thunderstorm")
+                           ("VA" . "volcanic ash")
+                           ("VC" . "vicinity"))
+  "Alist of codes and descriptions for METAR weather phenomenoa.")
+
+(defconst metar-phenomena-regexp
+  (eval `(rx symbol-start
+            (group (optional (char ?+ ?-)))
+            (group (1+ (or ,@(mapcar #'car metar-phenomena))))
+            symbol-end)))
+
+(defun metar-phenomena (info)
+  (when (string-match metar-phenomena-regexp info)
+    (let ((words ()))
+      (when (string= (match-string 1 info) "-")
+       (push "light" words))
+      (let ((obs (match-string 2 info)))
+       (while (> (length obs) 0)
+         (setq words (nconc words
+                            (list (cdr (assoc-string (substring obs 0 2)
+                                                     metar-phenomena))))
+               obs (substring obs 2))))
+      (mapconcat #'identity words " "))))
+
+(defconst metar-wind-regexp
+  (rx symbol-start
+      (group (or "VRB" (= 3 digit)))
+      (group (repeat 2 3 digit)) (optional (char ?G) (group (1+ digit)))
+      "KT"
+      symbol-end
+      (optional (one-or-more not-newline)
+               symbol-start
+               (group (= 3 digit)) (char ?V) (group (= 3 digit))
+               symbol-end))
+  "Regular expression to match wind information in METAR records.")
+
+(defsubst metar-knots (value)
+  (cons value 'knots))
+
+(defsubst metar-degrees (value)
+  (cons value 'degrees))
+
+(defun metar-wind (info)
+  (when (string-match metar-wind-regexp info)
+    (append
+     (if (string= (match-string 1 info) "VRB")
+        (when (and (match-string 4 info) (match-string 5 info))
+          (list :from (string-to-number (match-string 4 info))
+                :to (string-to-number (match-string 5 info))))
+       (append (list :direction (metar-degrees (string-to-number (match-string 
1 info))))
+              (when (and (match-string 4 info) (match-string 5 info))
+                (list :from (metar-degrees (string-to-number (match-string 4 
info)))
+                      :to (metar-degrees (string-to-number (match-string 5 
info)))))))
+     (list :speed (metar-knots (string-to-number (match-string 2 info))))
+     (when (match-string 3 info)
+       (list :gusts (metar-knots (string-to-number (match-string 3 info))))))))
+
+(defconst metar-visibility-regexp
+  (rx symbol-start (group (1+ digit)) (optional (group "SM")) symbol-end))
+
+(defconst metar-temperature-and-dewpoint-regexp
+  (rx symbol-start
+      (group (group (optional (char ?M))) (1+ digit))
+      (char ?/)
+      (group (group (optional (char ?M))) (1+ digit))
+      symbol-end)
+  "Regular expression to match temperature and dewpoint information in METAR 
records.")
+
+(defun metar-temperature (info)
+  (when (string-match metar-temperature-and-dewpoint-regexp info)
+    (cons (metar-temp-to-number (match-string 1 info)) 'celsius)))
+
+(defun metar-dewpoint (info)
+  (when (string-match metar-temperature-and-dewpoint-regexp info)
+    (cons (metar-temp-to-number (match-string 3 info)) 'celsius)))
+
+(defun metar-humidity (info)
+  (when (string-match metar-temperature-and-dewpoint-regexp info)
+    (cons (round
+          (metar-magnus-formula-humidity-from-dewpoint
+           (metar-temp-to-number (match-string 1 info))
+           (metar-temp-to-number (match-string 3 info)))) 'percent)))
+
+(defconst metar-pressure-regexp
+  (rx symbol-start (group (char ?Q ?A)) (group (1+ digit)) symbol-end)
+  "Regular expression to match air pressure information in METAR records.")
+
+(defun metar-pressure (info)
+  (when (string-match metar-pressure-regexp info)
+    (cons (string-to-number (match-string 2 info))
+         (if (string= (match-string 1 info) "Q") 'hPa 'inHg))))
+
+(defun metar-decode (record)
+  "Return a lisp structure describing the weather information in RECORD."
+  (when record
+    (let* ((codes (cdr record))
+          (temperature (metar-temperature codes))
+          (dewpoint (metar-dewpoint codes))
+          (humidity (metar-humidity codes))
+          (pressure (metar-pressure codes))
+          (wind (metar-wind codes)))
+      (append
+       (list (cons 'station (car (split-string codes " ")))
+            (cons 'timestamp (car record))
+            (cons 'wind wind)
+            (cons 'temperature temperature)
+            (cons 'dewpoint dewpoint)
+            (cons 'humidity humidity)
+            (cons 'pressure pressure))
+       (when (metar-phenomena codes)
+        (list 'phenomena (metar-phenomena codes)))))))
+
+(defun metar-magnus-formula-humidity-from-dewpoint (temperature dewpoint)
+  "Calculate relative humidity (in %) from TEMPERATURE and DEWPOINT (in
+degrees celsius)."
+  (* 10000
+     (expt 10
+          (- (/ (- (* 0.4343
+                      (+ 243.12 temperature)
+                      (/ (* dewpoint 17.62)
+                         (+ 243.12 dewpoint)))
+                   (* 0.4343 17.62 temperature))
+                (+ 243.12 temperature))
+             2))))
+
+;;;###autoload
+(defun metar (&optional arg)
+  "Display recent weather information.
+If a prefix argument is given, prompt for the exact station code.
+Otherwise, determine the best station via latitude/longitude."
+  (interactive "p")
+  (unless arg (setq arg 1))
+  (let (station)
+    (cond
+     ((= arg 1)
+      (unless calendar-longitude
+       (setq calendar-longitude
+             (solar-get-number
+              "Enter longitude (decimal fraction; + east, - west): ")))
+      (unless calendar-latitude
+       (setq calendar-latitude
+             (solar-get-number
+            "Enter latitude (decimal fraction; + north, - south): ")))
+      (when (and calendar-latitude calendar-longitude
+                (setq station (metar-find-station-by-latitude/longitude
+                               (calendar-latitude) (calendar-longitude))))
+       (message "Found %s %d kilometers away." (car station) (cdr station))
+       (setq station (car station))))
+     ((= arg 4)
+      (let* ((country (completing-read "Country: " (metar-station-countries) 
nil t))
+            (name (completing-read "Station name: " (mapcar (lambda (s) (cdr 
(assq 'name s)))
+                                                            
(metar-stations-in-country country))
+                                   nil t)))
+       (setq station (cdr (assq 'code (cl-find-if (lambda (s)
+                                                    (and (string= name (cdr 
(assq 'name s)))
+                                                         (string= country (cdr 
(assq 'country s)))))
+                                                  (metar-stations)))))))
+     ((= arg 16)
+      (setq station (completing-read "Enter METAR station code: "
+                                    (mapcar (lambda (station-info)
+                                              (cdr (assq 'code station-info)))
+                                            (metar-stations))
+                                    nil t))))
+    (let ((info (metar-decode (metar-get-record station))))
+      (if info
+         (message "%d minutes ago at %s: %d�C, %d%% relative humidity%s"
+                  (/ (truncate (float-time (time-since (cdr (assoc 'timestamp 
info))))) 60)
+                  (or (metar-stations-get (cdr (assoc 'station info)) 'name)
+                      (cdr (assoc 'station info)))
+                  (cadr (assoc 'temperature info))
+                  (cadr (assoc 'humidity info))
+                  (if (assoc 'phenomena info)
+                      (concat ", " (cdr (assoc 'phenomena info)))
+                    ""))
+       (message "No weather information found, sorry.")))))
+  
+(defun metar-station-countries ()
+  (let (countries (stations (metar-stations)))
+    (while stations
+      (let ((country (cdr (assq 'country (car stations)))))
+       (add-to-list 'countries country))
+      (setq stations (cdr stations)))
+    countries))
+
+(defun metar-stations-in-country (country)
+  (cl-loop for station-info in (metar-stations)
+          when (string= country (cdr (assq 'country station-info)))
+          collect station-info))
+
+(defun metar-average-temperature (country)
+  "Display average temperature from all stations in COUNTRY."
+  (interactive
+   (list (completing-read "Country: " (metar-station-countries) nil t)))
+  (let ((count 0) (temp-sum 0)
+       (stations (metar-stations))
+       (url-show-status nil)
+       (progress (make-progress-reporter
+                  "Downloading METAR records..."
+                  0
+                  (cl-count-if (lambda (station)
+                                 (string= (cdr (assoc 'country station))
+                                          country))
+                               (metar-stations)))))
+    (while stations
+      (when (string= (cdr (assoc 'country (car stations))) country)
+       (let ((temp (cdr (assoc 'temperature
+                               (metar-decode
+                                (metar-get-record
+                                 (cdr (assoc 'code (car stations)))))))))
+         (when temp
+           (setq temp-sum (+ temp-sum temp)
+                 count (+ count 1))
+           (progress-reporter-update progress count))))
+      (setq stations (cdr stations)))
+    (progress-reporter-done progress)
+    (if (called-interactively-p 'interactive)
+       (message "Average temperature in %s is %s"
+                country
+                (if (> count 0)
+                    (format "%.1f�C (%d stations)"
+                            (/ (float temp-sum) count)
+                            count)
+                  "unknown"))
+      (when (> count 0)
+       (/ (float temp-sum) count)))))
+
+(provide 'metar)
+;;; metar.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]