;;; geo - geographical data processing
;;;
;;; Copyright (C) 1998-2000 by Sam Steingold.
;;; This is Free Software, covered by the GNU GPL (v2)
;;; See http://www.gnu.org/copyleft/gpl.html
;;;
;;; $Id: geo.lisp,v 2.12 2001/11/02 22:31:15 sds Exp $
;;; $Source: /cvsroot/clocc/clocc/src/cllib/geo.lisp,v $
(eval-when (compile load eval)
(require :cllib-base (translate-logical-pathname "clocc:src;cllib;base"))
;; `index-t'
(require :cllib-withtype (translate-logical-pathname "cllib:withtype"))
;; `kwd'
(require :cllib-symb (translate-logical-pathname "cllib:symb"))
;; `save-restore', `skip-search', `skip-blanks', `skip-to-line',
;; `read-non-blanks'
(require :cllib-fileio (translate-logical-pathname "cllib:fileio"))
;; `strip-html-markup'
(require :cllib-html (translate-logical-pathname "cllib:html"))
;; `comma'
(require :cllib-tilsla (translate-logical-pathname "cllib:tilsla"))
;; `make-url'
(require :cllib-url (translate-logical-pathname "cllib:url")))
(in-package :cllib)
(export '(cite-info *weather-url* weather-report
find-country *country-list* fetch-country-list))
;;;
;;; {{{ Georgaphic Coordinates
;;;
(defun parse-geo-coord (st)
"Return the number parsed from latitude or longitude (dd:mm:ss[NSEW])
read from the stream."
(declare (stream st))
(let* ((sig 1) (cc (+ (read st) (/ (read st) 60d0)))
(lt (read st)) se nn)
(if (numberp lt) (setq se lt nn 0 lt (string (read st)))
(setf (values se nn)
(parse-integer (setq lt (string lt)) :junk-allowed t)))
(unless se (setq se 0))
(setq sig (cond ((or (char-equal (schar lt nn) #\n)
(char-equal (schar lt nn) #\e)) 1)
((or (char-equal (schar lt nn) #\s)
(char-equal (schar lt nn) #\w)) -1)
(t (error "Wrong sign designation: `~a'. ~
Must be one of [N]orth, [S]outh, [E]ast or [W]est." (schar lt nn)))))
(dfloat (* sig (+ cc (/ se 3600d0))))))
(defun geo-location (str &key (start 0))
"Return the latitude and longitude as two numbers from a string of
type \"48:51:00N 2:20:00E\". Return 2 values - latitude and longitude."
(declare (simple-string str) (fixnum start))
(setq str (nsubstitute #\Space #\: (string str))
str (nsubstitute #\Space #\, (string str)))
(with-input-from-string (st str :start start)
(complex (parse-geo-coord st) (parse-geo-coord st))))
(defun print-geo-coords (coord &optional (out *standard-output*))
"Print the geographic coordinates to the stream."
(declare (type (complex double-float) coord) (stream out))
(let ((lat (realpart coord)) (lon (imagpart coord)))
(declare (double-float lat lon))
(format out "~9,5f ~a ~9,5f ~a" lat (if (minusp lat) #\S #\N) lon
(if (minusp lon) #\W #\E))))
;;;
;;; }}}{{{ Geo-Data
;;;
(eval-when (compile load eval) ; CMUCL
(defstruct (geo-data (:conc-name geod-)
#+cmu (:print-function print-struct-object))
(name "??" :type string) ; the name of the place
(pop 0 :type (real 0)) ; population
(crd #C(0d0 0d0) :type (complex double-float)) ; coordinates
(zip nil :type list)) ; list of zip codes.
)
(defmethod print-object ((gd geo-data) (out stream))
"Print the geo-data."
(when *print-readably* (return-from print-object (call-next-method)))
(format out "Place: ~a~%Population: ~12:d;~30tLocation: "
(geod-name gd) (geod-pop gd))
(print-geo-coords (geod-crd gd) out)
(format out "~%Zip Code~p:~{ ~d~}~%" (length (geod-zip gd)) (geod-zip gd)))
(defcustom *census-gazetteer-url* url
(make-url :port 80 :prot :http :host "www.census.gov"
:path "/cgi-bin/gazetteer?")
"*The URL to use to get the cite information.")
;;;###autoload
(defun cite-info (&key (url *census-gazetteer-url*) city state zip
(out *standard-output*))
"Get the cite info from the U.S. Gazetteer.
Print the results to the stream OUT (defaults to T, discard if NIL)
and return a list of geo-data."
(setq url (if (url-p url) (copy-url url) (url url)))
(assert (or city state zip) (city state zip)
"You must specify at least one of :city, :state or :zip~%")
(flet ((prep (str) (if str (substitute #\+ #\Space (string str)) "")))
(setf (url-path url)
(format nil "~acity=~a&state=~a&zip=~a" (url-path url) (prep city)
(prep state) (or zip ""))))
(with-open-url (sock url :err *standard-output*)
(skip-search sock "
")
(do ((str "") res gd (ii 1 (1+ ii)))
((or (search "
" str)
(search "" (setq str (read-line sock))))
(nreverse res))
(declare (type index-t ii) (simple-string str))
;; name
(setq gd (make-geo-data :name (strip-html-markup str))
str (read-line sock))
;; population
(setf (geod-pop gd) (parse-integer str :junk-allowed t :start
(1+ (position #\: str))))
;; location
(setq str (nsubstitute #\Space #\: (read-line sock))
str (nsubstitute #\Space #\, str)
str (nsubstitute #\Space #\< str))
(with-input-from-string (st str)
(read st)
(setf (geod-crd gd)
(complex (* (read st) (if (eq 'n (read st)) 1 -1))
(* (read st) (if (eq 'w (read st)) 1 -1)))))
;; zip
(setq str (read-line sock))
(setf (geod-zip gd)
(if (search "Zip Code" str)
(with-input-from-string (st str :start (1+ (position #\: str)))
(do (rr re) ((null (setq rr (read st nil nil)))
(nreverse re))
(when (numberp rr) (push rr re))))
(list zip)))
(read-line sock) (setq str (read-line sock)) (push gd res)
(when out (format out "~%~:d. ~a" ii gd)))))
(defcustom *weather-url* url
(make-url :prot :telnet :port 3000 :host "mammatus.sprl.umich.edu")
"*The url for the weather information.")
;;;###autoload
(defun weather-report (code &key (out t) (url *weather-url*))
"Print the weather report for CODE to OUT."
(setq url (if (url-p url) (copy-url url) (url url)))
(setf (url-path url) (format nil "/~a//x" code))
(with-open-url (sock url)
(do (rr)
((null (setq rr (read-line sock nil nil))))
(format out "~a~%" rr))))
;;;
;;; Countries
;;;
(eval-when (compile load eval) ; CMUCL
(defstruct (country #+cmu (:print-function print-struct-object))
"The country structure - all the data about a country you can think of."
(name "" :type simple-string) ; name
(fips nil :type symbol) ; FIPS PUB 10-4 code (US Dept of State)
(iso2 nil :type symbol) ; ISO 3166: Codes for the Representation
(iso3 nil :type symbol) ; of Names of Countries. 2- and 3- letters
(isod 0 :type fixnum) ; ISO 3166: number
(inet nil :type symbol) ; Internet Domain
(incl nil :type (or null country)) ; Included in
(captl nil :type (or null simple-string)) ; Capital
(area 0d0 :type (double-float 0d0)) ; Area, in sq km
(frnt 0d0 :type (double-float 0d0)) ; fontier length, in km
(cstl 0d0 :type (double-float 0d0)) ; coastline, in km
(crd #C(0d0 0d0) :type (complex double-float)) ; coordinates
(pop 0 :type integer) ; population
(birth 0d0 :type (double-float 0d0)) ; birth rate
(death 0d0 :type (double-float 0d0)) ; death rate
(mgrtn 0d0 :type double-float) ; net migration rate
(fert 0d0 :type (double-float 0d0)) ; fertility rate per woman
(life 0d0 :type (double-float 0d0)) ; life expectancy at birth
(gdp 0d0 :type (double-float 0d0)) ; GDP, in $$
(gdpgr nil :type (or null double-float)) ; GDP growth, in %%
(gdppc 0d0 :type (double-float 0d0)) ; GDP per capita, in $$
(note nil :type (or null simple-string)) ; ISO Note
(lctn nil :type (or null simple-string)) ; Location Description
(dspt nil :type (or null simple-string)) ; Territorial Disputes
(clmt nil :type (or null simple-string)) ; Climate
(rsrc nil :type (or null simple-string)) ; Natural Resources
(ethn nil :type (or null simple-string)) ; ethnic divisions
(lang nil :type (or null simple-string)) ; languages
(rlgn nil :type (or null simple-string)) ; religions
))
(defmethod print-object ((ntn country) (out stream))
(when *print-readably* (return-from print-object (call-next-method)))
(format out "~a~@[ (~a)~] [~a ~a] [ISO: ~a ~a ~d]~@[ part of ~a~]
Location: "
(country-name ntn) (country-captl ntn) (country-fips ntn)
(country-inet ntn) (country-iso2 ntn) (country-iso3 ntn)
(country-isod ntn) (and (country-incl ntn)
(country-name (country-incl ntn))))
(print-geo-coords (country-crd ntn) out)
(format out "~%Population: ~15:d B: ~5f D: ~5f M: ~5f Net: ~5f
Fertility: ~5f births/woman Life expectancy at birth: ~5f years
Area: ~1/comma/ sq km Frontiers: ~1/comma/ km Coastline: ~1/comma/ km
GDP: ~2,15:/comma/ (~f $/cap~@[; %~4f growth~])
~@[ * Location: ~a~%~]~@[ * Disputes: ~a~%~]~
~@[ * Climate: ~a~%~]~@[ * Resources: ~a~%~]~@[ * Ethnic divisions: ~a~%~]~
~@[ * Languages: ~a~%~]~@[ * Religions: ~a~%~]~@[[~a]~%~]"
(country-pop ntn) (country-birth ntn) (country-death ntn)
(country-mgrtn ntn) (country-pop-chg ntn) (country-fert ntn)
(country-life ntn) (country-area ntn) (country-frnt ntn)
(country-cstl ntn) (country-gdp ntn) (country-gdppc ntn)
(country-gdpgr ntn) (country-lctn ntn) (country-dspt ntn)
(country-clmt ntn) (country-rsrc ntn) (country-ethn ntn)
(country-lang ntn) (country-rlgn ntn) (country-note ntn)))
(defsubst country-pop-chg (ntn)
"Return the net population change, per 1000."
(declare (type country ntn))
(- (+ (country-birth ntn) (country-mgrtn ntn)) (country-death ntn)))
(defcustom *geo-code-url* url
(make-url :prot :http :host "www.odci.gov" :path
"/cia/publications/factbook/appf.html")
"*The URL with the table of the codes.")
(defcustom *geo-info-template* simple-string
"http://www.odci.gov/cia/publications/factbook/~a.html"
"*The string for generating the URL for getting information on a country.")
(defcustom *country-list* list nil "*The list of known countries.")
;;;###autoload
(defsubst find-country (slot value &optional (ls *country-list*)
(test (cond ((member slot '(name cap note)) #'search)
((member slot '(isod pop)) #'=)
((member slot '(area frnt lat lon gdp))
(lambda (va sl) (< (car va) sl (cdr va))))
(#'eq))))
"Get the COUNTRY struct corresponding to the given SLOT VALUE.
Returns the list of all countries satisfying the condition.
Looks in list LS, which defaults to `*country-list*'. If slot value
is a float, such as the GDP, VALUE is a cons with the range.
(find-country SLOT VALUE &optional LS TEST)"
(declare (symbol slot) (type (function (t t) t) test) (list ls))
(remove-if-not (lambda (nn) (funcall test value (slot-value nn slot))) ls))
(defun save-restore-country-list (&optional (save t))
"Save or restore `*country-list*'."
(save-restore save :var '*country-list* :name "country.dat"
:basedir *datadir*))
(defun str-core (rr)
"Get the substring of the string from the first `>' to the last `<'."
(declare (simple-string rr))
(let ((cc (subseq rr (1+ (position #\> rr))
(position #\< rr :from-end t :start 2))))
(cond ((string= "-" cc) "NIL") ((string= "
" cc) nil)
((string-trim +whitespace+ cc)))))
(defun read-some-lines (st)
"Read some lines from tag to tag."
(declare (stream st))
(do* ((rr (read-line st) (read-line st))
(cp (position #\< rr :from-end t :start 2) (position #\< rr))
(cc (subseq rr (1+ (position #\> rr)) cp)
(concatenate 'string cc " " (subseq rr 0 cp))))
(cp (if (char= #\& (schar cc 0)) nil (string-trim +whitespace+ cc)))
(declare (simple-string rr cc))))
;;;###autoload
(defun fetch-country-list ()
"Initialize `*country-list*' from `*geo-code-url*'."
(format t "~&Reading `~a'" *geo-code-url*)
(with-open-url (st *geo-code-url* :err *standard-output*)
;; (with-open-file (st "/home/sds/lisp/wfb-appf.htm" :direction :input)
(skip-search st "Entity")
(skip-search st "")
(do (res)
((search "" (skip-blanks st) :test #'char=)
(setq *country-list* (nreverse res)))
(princ ".") (force-output)
(push (make-country
:name (read-some-lines st)
:fips (kwd (str-core (read-line st)))
:iso2 (kwd (str-core (read-line st)))
:iso3 (kwd (str-core (read-line st)))
:isod (or (read-from-string (str-core (read-line st))) 0)
:inet (kwd (str-core (read-line st)))
:note (read-some-lines st)) res)))
(format t "~d countries.~%" (length *country-list*))
(dolist (nn *country-list*) ; set incl
(when (country-note nn)
(format t "~a: ~a~%" (country-name nn) (country-note nn))
(do* ((iiw " includes with ") (iiwt " includes with the ") len
(pos (or (progn (setq len (length iiwt))
(search iiwt (country-note nn)))
(progn (setq len (length iiw))
(search iiw (country-note nn))))
(1+ (or (position #\Space new) (1- (length new)))))
(new (if pos (subseq (country-note nn) (+ len pos)))
(subseq new pos))
(ll (find-country 'name new) (find-country 'name new)))
((or (null new) (zerop (length new)) (= 1 (length ll)))
(if (= 1 (length ll))
(format t "~5tIncluding into --> ~a~%"
(country-name (setf (country-incl nn) (car ll))))
(if pos (format t "~10t *** Not found!~%"))))))))
(defun dump-country (ntn &rest opts)
"Dump the URL for the country."
(declare (type country ntn))
(apply #'dump-url (url (format nil *geo-info-template* (country-fips ntn)))
opts))
(defun view-country (&rest find-args)
(let ((ntn (if (country-p (car find-args)) (car find-args)
(car (apply #'find-country find-args)))))
(browse-url (url (format nil *geo-info-template* (country-fips ntn))))))
(defmacro dump-find-country ((&rest find-args)
(&rest dump-args &key (out *standard-output*)
&allow-other-keys))
"Dump all the URLs for all the relevant countries."
(remf dump-args :out)
(dolist (cc (apply #'find-country find-args))
(let ((st (if (or (and (symbolp out) (fboundp out)) (functionp out))
(funcall out cc) out)))
(declare (stream st))
(format st "~70~~%~a~70~~%" cc)
(apply #'dump-country cc :out st dump-args))))
(defun update-country (cc)
"Get the data from the WWW and update the structure."
(declare (type country cc))
;; (setf (country-note cc) (current-time nil))
(with-open-url (st (url (format nil *geo-info-template* (country-fips cc)))
:err *standard-output*)
(ignore-errors
(setf (country-lctn cc) (next-info st "Location:")
(country-crd cc)
(geo-location (next-info st "
Geographic coordinates:")))
(skip-to-line st "Area:")
(setf (country-area cc) (next-info st "
total:" 'float))
(let ((lb (next-info st "
Land boundaries:" 'number)))
(typecase lb
(number (setf (country-frnt cc) (dfloat lb)))
(t (setf (country-frnt cc) (dfloat (parse-num (read-line st))))
(add-note cc "Borders with: "
(next-info st "
border countr")))))
(setf (country-cstl cc) (next-info st "
Coastline:" 'float)
(country-dspt cc)
(next-info st "
International disputes:")
(country-clmt cc) (next-info st "
Climate:")
(country-rsrc cc) (next-info st "
Natural resources:")
(country-pop cc) (next-info st "Population:" 'number)
(country-birth cc) (next-info st "
Birth rate:" 'float)
(country-death cc) (next-info st "
Death rate:" 'float)
(country-mgrtn cc)
(next-info st "
Net migration rate:" 'float)
(country-life cc)
(next-info st "
Life expectancy at birth:" 'float)
(country-fert cc)
(next-info st "
Total fertility rate:"'float)
(country-ethn cc) (next-info st "
Ethnic groups:")
(country-rlgn cc) (next-info st "
Religions:")
(country-lang cc) (next-info st "
Languages:")
(country-captl cc) (next-info st "
Capital:")
(country-gdp cc) (next-info st "
GDP:" 'float)
(country-gdppc cc)
(next-info st "
GDP per capita:" 'float)
))
cc))
(defun next-info (str skip &optional type)
"Get the next object from stream STR, skipping till SKIP, of type TYPE."
(declare (stream str) (simple-string skip) (symbol type))
(let ((ln (skip-to-line str skip)))
(case type (float (dfloat (parse-num ln))) (number (parse-num ln))
(t (concatenate 'string ln (read-non-blanks str))))))
(defun add-note (cc &rest news)
"Append a note."
(declare (type country cc) (list news))
(setf (country-note cc)
(apply #'concatenate 'string (country-note cc)
#.(string #\Newline) news)))
;(defun true (&rest zz) (declare (ignore zz)) t)
;(defun false (&rest zz) (declare (ignore zz)) nil)
(defun parse-num (st)
"Parse the number from the string, fixing commas."
(declare (simple-string st))
(fill st #\Space :end
(let ((pp (position-if
(lambda (zz) (or (digit-char-p zz) (eql zz #\$))) st)))
(when pp (if (digit-char-p (char st pp)) pp (1+ pp)))))
(nsubstitute #\Space #\% st)
(do ((pos 0 (and next (1+ next))) next res)
((null pos)
(setf st (apply #'concatenate 'string (nreverse res))
(values next pos) (read-from-string st nil nil))
(and next
(* next (case (read-from-string st nil nil :start pos)
(trillion 1000000000000) (billion 1000000000)
(million 1000000) (t 1)))))
(declare (type (or null index-t) pos next))
(push (subseq st pos (setq next (position #\, st :start pos))) res)))
#+nil
(progn
(load "geo")
(fetch-country-list)
(dolist (cc *country-list*)
;(update-country cc)
(when (and (zerop (country-gdppc cc))
(/= 0 (country-gdp cc)) (/= 0 (country-pop cc)))
(setf (country-gdppc cc) (fround (country-gdp cc) (country-pop cc)))
(format t " *** ~a~2%" cc)))
(save-restore-country-list))
(provide :cllib-geo)
;;; geo.lisp ends here