;;; 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 "" 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