;;;; -*- mode:lisp;coding:utf-8 -*-
;;;;**************************************************************************
;;;;FILE:               apt.lisp
;;;;LANGUAGE:           Common-Lisp
;;;;SYSTEM:             Common-Lisp
;;;;USER-INTERFACE:     NONE
;;;;DESCRIPTION
;;;;
;;;;    Parses Airports/apt.dat.gz and retrieve information about airports.
;;;;
;;;;AUTHORS
;;;;    <PJB> Pascal J. Bourguignon <pjb@ogamita.com>
;;;;MODIFICATIONS
;;;;    2011-04-10 <PJB> Created.
;;;;BUGS
;;;;LEGAL
;;;;    GPL
;;;;
;;;;    Copyright Pascal J. Bourguignon 2011 - 2011
;;;;
;;;;    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
;;;;    2 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, write to the Free
;;;;    Software Foundation, Inc., 59 Temple Place, Suite 330,
;;;;    Boston, MA 02111-1307 USA
;;;;**************************************************************************


(defpackage "COM.OGAMITA.FGFS.APT"
  (:use "COMMON-LISP"
        "COM.OGAMITA.FGFS.GEOMETRY")

  (:export "*FGROOT*" "*AIRPORT-FILE*"

           "RUNWAY" "RUNWAY-P" "MAKE-RUNWAY" "COPY-RUNWAY"
           "RUNWAY-NAME" "RUNWAY-LONGITUDE" "RUNWAY-LATITUDE"
           "RUNWAY-ALTITUDE"

           "AIRPORT" "AIRPORT-P" "MAKE-AIRPORT" "COPY-AIRPORT"
           "AIRPORT-CODE" "AIRPORT-NAME" "AIRPORT-RUNWAYS"

           "LOAD-AIRPORTS"
           "GET-AIRPORT-BY-CODE"
           "SELECT-AIRPORTS-BY-NAME"
           "SELECT-AIRPORTS-CLOSE-TO")
  (:documentation "
Parses Airports/apt.dat.gz and retrieve information about airports.
GPL
Copyright Pascal J. Bourguignon 2011 - 2011
"))
(in-package "COM.OGAMITA.FGFS.APT")


(defstruct runway
  name
  longitude ; in degree.
  latitude ; in degree.
  direction ; in degree. true heading
  length) ; in meters, it seems


(defstruct ramp
  name
  longitude ; in degree.
  latitude ; in degree.
  direction) ; in degree. true heading

(defstruct tower
  name
  longitude ; in degree.
  latitude ; in degree.
  altitude ; feet agl.
  (present t :type boolean))

(defstruct airport
  code
  name
  elevation ; feet above MSL
  has-tower-p
  runways
  ramps
  towers)


(defun airport-longitude (airport)
  (average (mapcar (function runway-longitude) (airport-runways airport))))

(defun airport-latitude (airport)
  (average (mapcar (function runway-latitude) (airport-runways airport))))


(defvar *airports* (make-hash-table :test (function equal)))

(defvar *fgroot*        #P"/other/fgfs/")
(defvar *airport-file*  #P"Airports/apt.dat.gz")


(defun load-airports ()
  "
Loads the airports (1) and runways (10) from the apt file.
NOTE: This may be all wrong, I did it just watching the file.
REFERENCE:  <http://data.x-plane.com/file_specs/Apt810.htm>
"
  (with-open-stream (inp
                     #+clisp (error "Not implemented yet")
                     #+ccl (ccl:external-process-output-stream
                            (ccl:run-program "gunzip" '()
                                             :wait nil
                                             :input (merge-pathnames *airport-file* *fgroot* nil)
                                             :output :stream))
                     #-(or clisp ccl) (error "Not implemented yet"))
    (macrolet ((add-airport ()
                 `(when (and airport (airport-runways airport))
                    (setf (gethash (airport-code airport) *airports*) airport
                          airport nil)
                    (incf num-airports))))
      (let ((generator (read-line inp))
            (version   (read inp))
            (comment   (read-line inp)))
       (loop
          :with num-airports = 0
          :with airport
          :for line = (read-line inp nil nil)
          :while line
          :when (plusp (length line))
          :do (with-input-from-string (inp line)
                (let ((kind (read inp)))
                  (case kind
                    ((1)
                     (add-airport)
                     (let ((elevation (read inp nil nil))
                           (has-tower (not (zerop (read inp nil nil)))) ; ensure boolean
                           (ignore    (read inp nil nil))
                           (code      (with-input-from-string (back "\\")
                                        (read (make-concatenated-stream back inp) nil))))
                       (when code
                         (setf airport (make-airport
                                        :code (string code)
                                        :elevation elevation
                                        :has-tower-p has-tower
                                        :name (string-trim " " (read-line inp)))))))
                    ((10)
                     (when airport
                       (let ((longitude (read inp nil nil))
                             (latitude  (read inp nil nil))
                             (name      (string (with-input-from-string (back "\\")
                                                  (read (make-concatenated-stream back inp) nil))))
                             (direction (read inp nil nil))
                             (length    (read inp nil nil)))

                         (when (and (= 3 (length name))
                                    (digit-char-p (aref name 0))
                                    (digit-char-p (aref name 1))
                                    (alpha-char-p (aref name 2)))
                           (push (make-runway :name name
                                              :longitude longitude
                                              :latitude latitude
                                              :direction direction
                                              :length length)
                                 (airport-runways airport))))))
                    ((14)
                     (when airport
                       (let ((longitude (read inp nil nil))
                             (latitude  (read inp nil nil))
                             (altitude  (read inp nil nil))
                             (present   (if (<= version 810)
                                            t
                                            (not (zerop (read inp nil nil))))) ; ensure boolean
                             (name      (string-trim " " (read-line inp nil nil))))
                         (push (make-tower :name name
                                           :longitude longitude
                                           :latitude latitude
                                           :altitude altitude
                                           :present present)
                               (airport-towers airport)))))
                    ((15)
                     (when airport
                       (let ((longitude (read inp nil))
                             (latitude  (read inp nil))
                             (direction (read inp nil nil))
                             (name      (string-trim " " (read-line inp nil nil))))
                         (push (make-ramp :name name
                                          :longitude longitude
                                          :latitude latitude
                                          :direction direction)
                               (airport-ramps airport)))))
                    ((99) (loop-finish)))))
          :finally (progn
                     (add-airport)
                     (return num-airports)))))))


(defun GET-AIRPORT-BY-CODE (code)
  "
RETURN: the airport that has the given CODE.
CODE:   A string or symbol.  The code is upcased before indexing the table.
"
  (gethash (string-upcase code) *airports*))


(defun SELECt-AIRPORTS-BY-NAME (name)
  "
RETURN: A list of airports having NAME as substring in their names.
        The search is case insensitive.
"
  (let ((results '()))
   (maphash (lambda (code airport)
              (when (search name (airport-name airport) :test (function char-equal))
                  (push airport results)))
            *airports*)
   results))


(defun select-airports-close-to (longitude latitude &optional (radius 30.0d0))
  "
RETURN: A list of airports that are within the RADIUS nautical miles
        of the given LONGITUDE and LATITUDE. (Orthodromic distance).
"
  (let ((results '()))
    (maphash (lambda (code airport)
               (let ((distance (orthodromic-distance/nautical-miles longitude latitude
                                                                    (airport-longitude airport)
                                                                    (airport-latitude airport))))
                 (when (<= distance radius)
                   (push (cons distance airport) results))))
             *airports*)
    (let ((result  (sort results (function <) :key (function car))))
      (map-into result (function cdr) result))))


;;;; THE END ;;;;
ViewGit