Don't create image footprints if calibration data is un-usable
[phoros.git] / cli.lisp
blob798ee247e6ddd3717ec1fcf9b4959df365b46b1d
1 ;;; PHOROS -- Photogrammetric Road Survey
2 ;;; Copyright (C) 2010, 2011 Bert Burgemeister
3 ;;;
4 ;;; This program is free software; you can redistribute it and/or modify
5 ;;; it under the terms of the GNU General Public License as published by
6 ;;; the Free Software Foundation; either version 2 of the License, or
7 ;;; (at your option) any later version.
8 ;;;
9 ;;; This program is distributed in the hope that it will be useful,
10 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ;;; GNU General Public License for more details.
13 ;;;
14 ;;; You should have received a copy of the GNU General Public License along
15 ;;; with this program; if not, write to the Free Software Foundation, Inc.,
16 ;;; 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 ;;;; The UNIX command line interface
21 ;; TODO: options that have a function as their :action seem to mask earlier options. Fix and remove (*) stuff.
23 (in-package :phoros)
25 (defparameter cli:*general-options*
26 '((("help" #\h) :action #'cli:help-action
27 :documentation "(*) Print this help and exit.")
28 (("licence" "license") :action #'cli:licence-action
29 :documentation "(*) Print licence boilerplate and exit.")
30 ("version" :action #'cli:version-action
31 :documentation "(*) Print version information and exit. Use --verbose=1 to see more. In a version string A.B.C, changes in A denote incompatible changes in data; changes in B mean user-visible changes in feature set.")
32 ("verbose" :type integer :initial-value 0
33 :documentation "Dependent on bits set in this integer, emit various kinds of debugging output. ")
34 ("log-dir" :type string :initial-value ""
35 :documentation "Where to put the log files. Created if necessary; should end with a slash.")
36 ("check-db" :action #'cli:check-db-action
37 :documentation "(*) Check connection to databases (including auxiliary if applicable) and exit.")
38 ("check-dependencies" :action #'cli:check-dependencies-action
39 :documentation "(*) Check presence of dependencies on local system and exit.")
40 ("nuke-all-tables" :action #'cli:nuke-all-tables-action
41 :documentation "(*) Ask for confirmation, then delete anything in database and exit.")
42 ("create-sys-tables" :action #'cli:create-sys-tables-action
43 :documentation "(*) Ask for confirmation, then create in database a set of sys-* tables (tables shared between all projects). The database should probably be empty before you try this.")))
45 (defparameter cli:*db-connection-options*
46 '((("host" #\H) :type string :initial-value "localhost"
47 :documentation "Database server.")
48 (("port" #\P) :type integer :initial-value 5432
49 :documentation "Port on database server.")
50 (("database" #\D) :type string :initial-value "phoros"
51 :documentation "Name of database.")
52 (("user" #\U) :type string
53 :documentation "Database user.")
54 (("password" #\W) :type string
55 :documentation "Database user's password.")
56 ("use-ssl" :type string :initial-value "no"
57 :documentation "Use SSL in database connection. [yes|no|try]")))
59 (defparameter cli:*aux-db-connection-options*
60 '(("aux-host" :type string
61 :documentation "Auxiliary database server. (default: same as --host)")
62 ("aux-port" :type integer
63 :documentation "Port on auxiliary database server. (default: same as --port)")
64 ("aux-database" :type string
65 :documentation "Name of auxiliary database. (defaul: same as --database)")
66 ("aux-user" :type string
67 :documentation "Auxiliary database user. (default: same as --user)")
68 ("aux-password" :type string
69 :documentation "Auxiliary database user's password. (default: same as --password)")
70 ("aux-use-ssl" :type string
71 :documentation "Use SSL in auxiliary database connection. [yes|no|try] (default: same as --use-ssl)")))
73 (defparameter cli:*get-image-options*
74 '(("get-image" :action #'cli:get-image-action
75 :documentation "(*) Get a single image from a .pictures file, print its trigger-time to stdout, and exit.")
76 ("count" :type integer :initial-value 0
77 :documentation "Image number in .pictures file.")
78 ("byte-position" :type integer
79 :documentation "Byte position of image in .pictures file.")
80 ("in" :type string
81 :documentation "Path to .pictures file.")
82 ("out" :type string :initial-value "phoros-get-image.png"
83 :documentation "Path to to output .png file.")
84 ;; The way it should be had we two-dimensional arrays in postmodern:
85 ;;("bayer-pattern" :type string :list t :optional t :action :raw-bayer-pattern :documentation "The first pixels of the first row. Repeat this option to describe following row(s). Each pixel is to be interpreted as RGB hex string. Example: use #ff0000,#00ff00 if the first pixels in topmost row are red, green.")
86 ("bayer-pattern" :type string :initial-value "#ff0000,#00ff00" :action :raw-bayer-pattern
87 :documentation "The first pixels of the first row. Each pixel is to be interpreted as RGB hex string. Example: use #ff0000,#00ff00 if the first pixels in topmost row are red, green.")))
89 (defparameter cli:*camera-hardware-options*
90 '(("store-camera-hardware" :action #'cli:store-camera-hardware-action
91 :documentation "(*) Put new camera-hardware data into the database; print camera-hardware-id to stdout.")
92 ("sensor-width-pix" :type integer
93 :documentation "Width of camera sensor.")
94 ("sensor-height-pix" :type integer
95 :documentation "Height of camera sensor.")
96 ("pix-size" :type string
97 :documentation "Camera pixel size in millimetres (float).")
98 ("channels" :type integer
99 :documentation "Number of color channels")
100 ("pix-depth" :type integer :initial-value 255
101 :documentation "Greatest possible pixel value.")
102 ("color-raiser" :type string :initial-value "1,1,1"
103 :action :raw-color-raiser
104 :documentation "Multipliers for the individual color components. Example: 1.2,1,.8 multiplies red by 1.2 and blue by 0.8.")
105 ;; The way it should be had we two-dimensional arrays in postmodern:
106 ;;("bayer-pattern" :type string :list t :optional t :action :raw-bayer-pattern :documentation "The first pixels of the first row. Repeat this option to describe following row(s). Each pixel is to be interpreted as RGB hex string. Example: use #ff0000,#00ff00 if the first pixels in topmost row are red, green.")
107 ("bayer-pattern" :type string :optional t
108 :action :raw-bayer-pattern
109 :documentation "The first pixels of the first row. Each pixel is to be interpreted as RGB hex string. Example: use #ff0000,#00ff00 if the first pixels in topmost row are red, green.")
110 ("serial-number" :type string
111 :documentation "Serial number.")
112 ("description" :type string
113 :documentation "Description of camera.")
114 ("try-overwrite" :type boolean :initial-value "yes"
115 :documentation "Overwrite matching camera-hardware record if any.")))
117 (defparameter cli:*lens-options*
118 '(("store-lens" :action #'cli:store-lens-action
119 :documentation "(*) Put new lens data into the database; print lens-id to stdout.")
120 ("c" :type string
121 :documentation "Nominal focal length in millimetres.")
122 ("serial-number" :type string
123 :documentation "Serial number.")
124 ("description" :type string
125 :documentation "Lens desription.")
126 ("try-overwrite" :type boolean :initial-value "yes"
127 :documentation "Overwrite matching lens record if any.")))
129 (defparameter cli:*generic-device-options*
130 '(("store-generic-device" :action #'cli:store-generic-device-action
131 :documentation "(*) Put a newly defined generic-device into the database; print generic-device-id to stdout.")
132 ("camera-hardware-id" :type integer
133 :documentation "Numeric camera hardware id in database.")
134 ("lens-id" :type integer
135 :documentation "Numeric lens id in database.")))
137 (defparameter cli:*device-stage-of-life-options*
138 '(("store-device-stage-of-life" :action #'cli:store-device-stage-of-life-action
139 :documentation "(*) Put a newly defined device-stage-of-life into the database; print device-stage-of-life-id to stdout.")
140 ("recorded-device-id" :type string
141 :documentation "Device id stored next to the measuring data.")
142 ("event-number" :type string
143 :documentation "GPS event that triggers this generic device.")
144 ("generic-device-id" :type integer
145 :documentation "Numeric generic-device id in database.")
146 ("vehicle-name" :type string
147 :documentation "Descriptive name of vehicle.")
148 ("casing-name" :type string
149 :documentation "Descriptive name of device casing.")
150 ("computer-name" :type string
151 :documentation "Name of the recording device.")
152 ("computer-interface-name" :type string
153 :documentation "Interface at device.")
154 ("mounting-date" :type string
155 :documentation "Time this device constellation became effective. Format: \"2010-11-19T13:49+01\".")))
157 (defparameter cli:*device-stage-of-life-end-options*
158 '(("store-device-stage-of-life-end" :action #'cli:store-device-stage-of-life-end-action
159 :documentation "(*) Put an end date to a device-stage-of-life in the database; print device-stage-of-life-id to stdout.")
160 ("device-stage-of-life-id" :type string
161 :documentation "Id of the device-stage-of-life to put to an end.")
162 ("unmounting-date" :type string
163 :documentation "Time this device constellation ceased to be effective. Format: \"2010-11-19T17:02+01\".")))
165 (defparameter cli:*camera-calibration-options*
166 '(("store-camera-calibration" :action #'cli:store-camera-calibration-action
167 :documentation "(*) Put new camera-calibration into the database; print generic-device-id and calibration date to stdout.")
168 ("device-stage-of-life-id" :type string
169 :documentation "This tells us what hardware this calibration is for.")
170 ("date" :type string
171 :documentation "Date of calibration. Format: \"2010-11-19T13:49+01\".")
172 ("person" :type string
173 :documentation "Person who did the calibration.")
174 ("main-description" :type string
175 :documentation "Regarding this entire set of calibration data")
176 ("usable" :type string :initial-value "yes"
177 :documentation "Set to no to just display images and inhibit photogrammetric calculations.")
178 ("debug" :type string
179 :documentation "If true: not for production use; may be altered or deleted at any time.")
180 ("photogrammetry-version" :type string
181 :documentation "Software version used to create this data.")
182 ("mounting-angle" :type integer
183 :documentation "Head up = 0; right ear up = 90; left ear up = -90; head down = 180.")
184 ("inner-orientation-description" :type string
185 :documentation "Comments regarding inner orientation calibration.")
186 ("c" :type string :documentation "Inner orientation: focal length.")
187 ("xh" :type string
188 :documentation "Inner orientation: principal point displacement.")
189 ("yh" :type string
190 :documentation "Inner orientation: principal point displacement.")
191 ("a1" :type string :documentation "Inner orientation: radial distortion.")
192 ("a2" :type string :documentation "Inner orientation: radial distortion.")
193 ("a3" :type string :documentation "Inner orientation: radial distortion.")
194 ("b1" :type string
195 :documentation "Inner orientation: asymmetric and tangential distortion.")
196 ("b2" :type string
197 :documentation "Inner orientation: asymmetric and tangential distortion.")
198 ("c1" :type string
199 :documentation "Inner orientation: affinity and shear distortion.")
200 ("c2" :type string
201 :documentation "Inner orientation: affinity and shear distortion.")
202 ("r0" :type string :documentation "Inner orientation.")
203 ("outer-orientation-description" :type string
204 :documentation "Comments regarding outer orientation calibration.")
205 ("dx" :type string :documentation "Outer orientation; in metres.")
206 ("dy" :type string :documentation "Outer orientation; in metres.")
207 ("dz" :type string :documentation "Outer orientation; in metres.")
208 ("omega" :type string :documentation "Outer orientation.")
209 ("phi" :type string :documentation "Outer orientation.")
210 ("kappa" :type string :documentation "Outer orientation.")
211 ("boresight-description" :type string
212 :documentation "Comments regarding boresight alignment calibration.")
213 ("b-dx" :type string :documentation "Boresight alignment.")
214 ("b-dy" :type string :documentation "Boresight alignment.")
215 ("b-dz" :type string :documentation "Boresight alignment.")
216 ("b-ddx" :type string :documentation "Boresight alignment.")
217 ("b-ddy" :type string :Documentation "Boresight alignment.")
218 ("b-ddz" :type string :documentation "Boresight alignment.")
219 ("b-rotx" :type string :documentation "Boresight alignment.")
220 ("b-roty" :type string :documentation "Boresight alignment.")
221 ("b-rotz" :type string :documentation "Boresight alignment.")
222 ("b-drotx" :type string :documentation "Boresight alignment.")
223 ("b-droty" :type string :documentation "Boresight alignment.")
224 ("b-drotz" :type string :documentation "Boresight alignment.")
225 ("nx" :type string
226 :documentation "X component of unit vector of vehicle ground plane.")
227 ("ny" :type string
228 :documentation "Y component of unit vector of vehicle ground plane.")
229 ("nz" :type string
230 :documentation "Z component of unit vector of vehicle ground plane.")
231 ("d" :type string :documentation "Distance of vehicle ground plane.")))
233 (defparameter cli:*acquisition-project-options*
234 '(("create-acquisition-project"
235 :type string :action #'cli:create-acquisition-project-action
236 :documentation "(*) Create a fresh set of canonically named data tables. The string argument is the acquisition project name. It will be stored in table sys-acquisition-project, field common-table-name, and used as a common part of the data table names.")
237 ("delete-acquisition-project"
238 :type string :action #'cli:delete-acquisition-project-action
239 :documentation "(*) Ask for confirmation, then delete acquisition project and all its measurements.")
240 ("delete-measurement"
241 :type integer :action #'cli:delete-measurement-action
242 :documentation "(*) Delete a measurement by its ID.")
243 ("list-acquisition-project"
244 :type string :optional t :action #'cli:list-acquisition-project-action
245 :documentation "(*) List measurements of one acquisition project if its name is specified, or of all acquisition projects otherwise.")))
247 (defparameter cli:*store-images-and-points-options*
248 '((("store-images-and-points" #\s) :type string :action #'cli:store-images-and-points-action
249 :documentation "(*) Link images to GPS points; store both into their respective DB tables. Images become linked to GPS points when their respective times differ by less than epsilon seconds, and when the respective events match. The string argument is the acquisition project name.")
250 (("directory" #\d) :type string
251 :documentation "Directory containing one set of measuring data.")
252 (("common-root" #\r) :type string
253 :documentation "The root part of directory that is equal for all pojects. TODO: come up with some sensible default.")
254 ("epsilon" :type string :initial-value ".001"
255 :documentation "Difference in seconds below which two timestamps are considered equal.")
256 ("aggregate-events" :type nil
257 :documentation "Put all GPS points in one bucket, disregarding any event numbers. Use this if you have morons setting up your generic-device. Hundreds of orphaned images may indicate this is the case.")
258 ("insert-footprints" :type string :action #'cli:insert-footprints-action
259 :documentation "(*) Update image footprints (the area on the ground that is most probably covered by the respective image). The string argument is the acquisition project name.")))
261 (defparameter cli:*start-server-options*
262 '(("server" :action #'cli:server-action
263 :documentation "(*) Start HTTP presentation server. Entry URI is http://<host>:<port>/phoros/<presentation-project>. Asynchronously update lacking image footprints (which should have been done already using --insert-footprints).")
264 ("address" :type string
265 :documentation "Address (of local machine) server is to listen to. Default is listening to all available addresses.")
266 ("http-port" :type integer :initial-value 8080
267 :documentation "Port the presentation server listens on.")
268 (("common-root" #\r) :type string :initial-value "/"
269 :documentation "The root part of directory that is equal for all pojects. TODO: come up with some sensible default.")
270 ("images" :type integer :initial-value 4 :action *number-of-images*
271 :documentation "Number of photos shown to the HTTP client.")
272 ("login-intro" :type string :list t :optional t :action *login-intro*
273 :documentation "Text to be shown below the login form. Use repeatedly to divide text into paragraphs. You can use HTML markup as long as it is legal inside <p>...</p>")))
275 (defparameter cli:*presentation-project-options*
276 '(("create-presentation-project"
277 :type string :action #'cli:create-presentation-project-action
278 :documentation "(*) Create a fresh presentation project which is to expose a set of measurements to certain users.")
279 ("delete-presentation-project"
280 :type string :action #'cli:delete-presentation-project-action
281 :documentation "(*) Ask for confirmation, then delete the presentation project including its table of user-generated points.")
282 ("list-presentation-project"
283 :type string :optional t :action #'cli:list-presentation-project-action
284 :documentation "(*) List one presentation project if specified, or all presentation projects if not.")
285 ("add-to-presentation-project"
286 :type string :action #'cli:add-to-presentation-project-action
287 :documentation "(*) Add to the presentation project given either certain measurements or all measurements currently in a certain acquisition project.")
288 ("remove-from-presentation-project"
289 :type string :action #'cli:remove-from-presentation-project-action
290 :documentation "(*) Remove from the presentation project given either certain measurements or all measurements currently in a certain acquisition project.")
291 ("measurement-id" :type integer :list t :optional t
292 :documentation "One measurement-id to add or remove. Repeat if necessary.")
293 ("acquisition-project"
294 :type string
295 :documentation "The acquisition project whose measurements are to add or remove.")
296 ("redefine-trigger-function"
297 :type string :action #'cli:redefine-trigger-function-action
298 :documentation "(*) Change body of the trigger function that is fired on changes to the user point table connected to the specified presentation project.")
299 ("plpgsql-body"
300 :type string
301 :documentation "Path to a file containing the body of a PL/pgSQL trigger function. Any ocurrence of the strings ~0@*~A and ~1@*~A will be replaced by the name of the user point table/of the user line table respectively. Omit this option to reset that function to just emit a notice.")))
303 (defparameter cli:*aux-view-options*
304 '(("create-aux-view"
305 :type string :action #'cli:create-aux-view-action
306 :documentation "(*) Connect table of auxiliary data with the specified presentation project by creating a view.")
307 ("aux-table"
308 :type string
309 :documentation "Name of auxiliary table. It may reside either in Phoros' native database or in an auxiliary database (which is common to all projects). It must have a geometry column.")
310 ("coordinates-column"
311 :type string :initial-value "the-geom"
312 :documentation "Name of the geometry column (which should have an index) in the auxiliary data table.")
313 ("numeric-column"
314 :type string :list t :optional t
315 :documentation "Name of a numeric column in the auxiliary data table. Repeat if necessary.")
316 ("text-column"
317 :type string :list t :optional t
318 :documentation "Name of a text column in the auxiliary data table. Repeat if necessary.")))
320 (defparameter cli:*user-points-options*
321 '(("get-user-points"
322 :type string :action #'cli:get-user-points-action
323 :documentation "(*) Save user points of presentation project.")
324 ("store-user-points"
325 :type string :action #'cli:store-user-points-action
326 :documentation "(*) Store user points previously saved (using --get-user-points or download button in Web interface) into the presentation project named by the string argument.")
327 ("json-file"
328 :type string
329 :documentation "Path to GeoJSON file.")))
331 (defparameter cli:*user-options*
332 '(("create-user"
333 :type string :action #'cli:create-user-action
334 :documentation "(*) Create or update user (specified by their alphanummeric ID) of certain presentation projects, deleting any pre-existing permissions of that user.")
335 ("user-password" :type string :documentation "User's password.")
336 ("user-full-name" :type string :documentation "User's real name.")
337 ("user-role"
338 :type string :initial-value "read"
339 :documentation "User's permission on their projects. One of \"read\", \"write\", or \"admin\" where \"write\" is the same as \"read\" plus permission to add user points and delete them if written by same user; and \"admin\" is the same as \"write\" plus permission to delete points written by other users.")
340 ("presentation-project" :type string :list t :optional t
341 :documentation "Presentation project the user is allowed to see. Repeat if necessary.")
342 ("delete-user"
343 :type string :action #'cli:delete-user-action
344 :documentation "(*) Delete user.")
345 ("list-user"
346 :type string :optional t :action #'cli:list-user-action
347 :documentation "(*) List the specified user with their presentation projects, or all users if no user is given.")))
349 (defparameter cli:*options*
350 (append cli:*general-options*
351 cli:*db-connection-options* cli:*aux-db-connection-options*
352 cli:*get-image-options*
353 cli:*camera-hardware-options* cli:*lens-options*
354 cli:*generic-device-options* cli:*device-stage-of-life-options*
355 cli:*device-stage-of-life-end-options*
356 cli:*camera-calibration-options*
357 cli:*acquisition-project-options*
358 cli:*store-images-and-points-options*
359 cli:*start-server-options*
360 cli:*presentation-project-options*
361 cli:*aux-view-options*
362 cli:*user-points-options*
363 cli:*user-options*))
365 (defun cli:main ()
366 "The UNIX command line entry point."
367 (handler-bind
368 ((serious-condition
369 (lambda (c)
370 (cl-log:log-message
371 :error "~A ~:[~;[Backtrace follows]~&~A~]~&"
373 hunchentoot:*log-lisp-backtraces-p*
374 (trivial-backtrace:print-backtrace c :output nil))
375 (format *error-output* "~A~&" c)
376 #+sbcl (sb-ext:quit :unix-status 1)))
377 (warning
378 (lambda (c) (cl-log:log-message :warning "~A" c))))
379 (cffi:use-foreign-library phoml)
380 (cli:compute-and-process-command-line-options cli:*options*)))
382 (defmacro cli:with-options ((&rest options) &body body)
383 "Evaluate body with options bound to the values of the respective
384 command line arguments. Elements of options may be either symbols or
385 lists shaped like (symbol default)."
386 `(destructuring-bind (&key ,@options &allow-other-keys)
387 (cli:remaining-options)
388 ,@body))
390 (defun cli:remaining-options ()
391 "Return current set of command line options as an alist, and a list
392 of the non-option arguments. In passing, set global variables
393 according to the --verbose option given."
394 (let ((options
395 (multiple-value-list
396 (cli:process-command-line-options
397 cli:*options* cli:*command-line-arguments*))))
398 (destructuring-bind (&key verbose &allow-other-keys)
399 (car options)
400 (setf *postgresql-warnings* (logbitp 11 verbose))
401 ;;(setf hunchentoot:*show-lisp-backtraces-p* (logbitp 12 verbose)) ;doesn't seem to exist
402 ;; obeyed by both hunchentoot and Phoros' own logging:
403 (setf hunchentoot:*log-lisp-backtraces-p* (logbitp 13 verbose))
404 ;; necessary for (ps ... (debug-info ...)...); doesn't work with
405 ;; (OpenLayers 2.10 AND Firefox 4), though:
406 (setf *use-multi-file-openlayers* (logbitp 14 verbose))
407 (setf *ps-print-pretty* (logbitp 15 verbose))
408 (setf hunchentoot:*show-lisp-errors-p* (logbitp 16 verbose)))
409 (values-list options)))
411 (defun cli:help-action (&rest rest)
412 "Print --help message."
413 (declare (ignore rest))
414 (flet ((show-help-section
415 (options-specification
416 &optional heading
417 &rest introduction-paragraphs)
418 "Show on *standard-output* help on options-specification
419 preceded by header and introduction-paragraphs."
420 (format *standard-output*
421 "~@[~2&_____~72,,,'_@<~A~>~]~
422 ~@[~{~& ~{~@<~% ~1,72:;~A~> ~}~}~]"
423 heading
424 (mapcar
425 #'(lambda (paragraph)
426 (cl-utilities:split-sequence-if
427 #'(lambda (x) (or (eql #\Space x)
428 (eql #\Newline x)))
429 paragraph
430 :remove-empty-subseqs t))
431 introduction-paragraphs))
432 (cli:show-option-help options-specification)))
433 (format
434 *standard-output*
435 "~&Usage: phoros option[=value] ...~&~A~2&"
436 (handler-bind ((warning #'ignore-warnings))
437 (asdf:system-long-description (asdf:find-system :phoros))))
438 (show-help-section
439 nil nil
440 "Options marked (*) are mutually exclusive and must come before
441 any other options.")
442 (show-help-section
443 cli:*general-options*
444 "General Options")
445 (show-help-section
446 cli:*db-connection-options*
447 "Database Connection"
448 "Necessary for most operations.")
449 (show-help-section
450 cli:*aux-db-connection-options*
451 "Auxiliary Database Connection"
452 "Connection parameters to the database containing auxiliary data.
453 Only needed for definition (--create-aux-view) and use (--server)
454 of auxiliary data.")
455 (show-help-section
456 cli:*get-image-options*
457 "Examine .pictures File"
458 "Useful mostly for debugging purposes.")
459 (show-help-section
460 cli:*camera-hardware-options*
461 "Camera Hardware Parameters"
462 "These do not include information on lenses or
463 mounting)")
464 (show-help-section
465 cli:*lens-options*
466 "Lens Parameters"
467 "Stored primarily for human consumption; not used in
468 photogrammetric calculations.")
469 (show-help-section
470 cli:*generic-device-options*
471 "Generic Device Definition"
472 "Basically, this is a particular camera fitted with a particular
473 lens.")
474 (show-help-section
475 cli:*device-stage-of-life-options*
476 "Device Stage-Of-Life Definition"
477 "A stage-of-life of a generic device is a possibly unfinished
478 period of time during which the mounting constellation of the
479 generic device remains unchanged.")
480 (show-help-section
481 cli:*device-stage-of-life-end-options*
482 "Put An End To A Device's Stage-Of-Life"
483 "This should be done after any event that renders any portion of
484 the calibration data invalid. E.g.: accidental change of mounting
485 constellation.")
486 (show-help-section
487 cli:*camera-calibration-options*
488 "Camera Calibration Parameters")
489 (show-help-section
490 cli:*acquisition-project-options*
491 "Manage Acquisition Projects"
492 (format nil
493 "An acquisition project is a set of measurements which
494 share a set of data tables and views named like ~(~A, ~A, ~A~)."
495 (point-data-table-name '<acquisition-project-name>)
496 (image-data-table-name '<acquisition-project-name>)
497 (aggregate-view-name '<acquisition-project-name>)))
498 (show-help-section
499 cli:*store-images-and-points-options*
500 "Store Measure Data")
501 (show-help-section
502 cli:*start-server-options*
503 "Become A HTTP Presentation Server"
504 "Phoros is a Web server in its own right, but you can also put it
505 behind a proxy server to make it part of a larger Web site.
506 E.g., for Apache, load module proxy_http and use this
507 configuration:"
508 "ProxyPass /phoros http://127.0.0.1:8080/phoros"
509 "ProxyPassReverse /phoros http://127.0.0.1:8080/phoros")
510 (show-help-section
511 cli:*presentation-project-options*
512 "Manage Presentation Projects"
513 "A presentation project is a set of measurements that can be
514 visited under a dedicated URL
515 \(http://<host>:<port>/phoros/<presentation-project>).
516 Its extent may or may not be equal to the extent of an
517 acquisition project."
518 "Presentation projects have a table of user points and a table of
519 user lines. The former is associated with a trigger which may be
520 defined to induce writing into the latter.")
521 (show-help-section
522 cli:*aux-view-options*
523 "Connect A Presentation Project To A Table Of Auxiliary Data"
524 (format nil
525 "Arbitrary data from tables not directly belonging to any
526 Phoros project can be connected to a presentation project by
527 means of a view named ~(~A~) with
528 columns coordinates (geometry), aux-numeric (null or array
529 of numeric), and aux-text (null or array of text)."
530 (aux-point-view-name '<presentation-project-name>))
531 "The array elements of both aux-numeric and aux-text of auxiliary
532 points can then be incorporated into neighbouring user points
533 during user point creation."
534 (format nil
535 "Also, a walk mode along auxiliary points becomes
536 available to the HTTP client. PL/pgSQL function ~(~A~) is
537 created to this end."
538 (thread-aux-points-function-name '<presentation-project-name>))
539 "In order to be accessible by Phoros, auxiliary data must be
540 structured rather simple (a single table which has a geometry
541 column and some numeric and/or text columns). You may want to
542 create a simplifying view if your data looks more complicated.")
543 (show-help-section
544 cli:*user-points-options*
545 "Manage User Points"
546 "Backup/restore of user points; especially useful for getting
547 them through database upgrades.")
548 (show-help-section
549 cli:*user-options*
550 "Manage Presentation Project Users")))
552 (defun cli:version-action (&rest rest)
553 "Print --version message. TODO: OpenLayers, Proj4js version."
554 (declare (ignore rest))
555 (cli:with-options (verbose)
556 (case verbose
558 (format
559 *standard-output*
560 "~&~A~&" (phoros-version)))
561 (otherwise
562 (format
563 *standard-output*
564 "~&~A version ~A~& ~A version ~A~& ~
565 Proj4 library: ~A~& PhoML version ~A~&"
566 (handler-bind ((warning #'ignore-warnings))
567 (asdf:system-description (asdf:find-system :phoros)))
568 (phoros-version)
569 (lisp-implementation-type) (lisp-implementation-version)
570 (proj:version)
571 (phoml:get-version-number))))))
573 (defun cli:licence-action (&rest rest)
574 "Print --licence boilerplate."
575 (declare (ignore rest))
576 (format
577 *standard-output* "~&~A~&"
578 (handler-bind ((warning #'ignore-warnings))
579 (asdf:system-licence (asdf:find-system :phoros)))))
581 (defun cli:check-db-action (&rest rest)
582 "Say `OK´ if database is accessible."
583 (declare (ignore rest))
584 (cli:with-options (host (aux-host host) port (aux-port port)
585 database (aux-database database)
586 (user "") (aux-user user)
587 (password "") (aux-password password)
588 use-ssl (aux-use-ssl use-ssl))
589 (when (and
590 (check-db (list database user password host
591 :port port
592 :use-ssl (s-sql:from-sql-name use-ssl)))
593 (check-db (list aux-database aux-user aux-password aux-host
594 :port aux-port
595 :use-ssl (s-sql:from-sql-name aux-use-ssl))))
596 (format *error-output* "~&OK~%"))))
598 (defun cli:check-dependencies-action (&rest rest)
599 "Say OK if the necessary external dependencies are available."
600 (declare (ignore rest))
601 (check-dependencies))
603 (defun cli:nuke-all-tables-action (&rest rest)
604 "Drop the bomb. Ask for confirmation first."
605 (declare (ignore rest))
606 (cli:with-options (host port database (user "") (password "") use-ssl
607 log-dir)
608 (launch-logger log-dir)
609 (when (yes-or-no-p
610 "You asked me to delete anything in database ~A at ~A:~D. ~
611 Proceed?"
612 database host port)
613 (with-connection (list database user password host :port port
614 :use-ssl (s-sql:from-sql-name use-ssl))
615 (muffle-postgresql-warnings)
616 (nuke-all-tables))
617 (cl-log:log-message
618 :db-sys "Nuked database ~A at ~A:~D. Back to square one!"
619 database host port))))
621 (defun cli:create-sys-tables-action (&rest rest)
622 "Make a set of sys-* tables. Ask for confirmation first."
623 (declare (ignore rest))
624 (cli:with-options (host port database (user "") (password "") use-ssl
625 log-dir)
626 (launch-logger log-dir)
627 (when (yes-or-no-p
628 "You asked me to create a set of sys-* tables ~
629 in database ~A at ~A:~D. ~
630 Make sure you know what you are doing. Proceed?"
631 database host port)
632 (with-connection (list database user password host :port port
633 :use-ssl (s-sql:from-sql-name use-ssl))
634 (muffle-postgresql-warnings)
635 (create-sys-tables))
636 (cl-log:log-message
637 :db-sys "Created a fresh set of system tables in database ~A at ~A:~D."
638 database host port))))
640 (defun cli:create-acquisition-project-action (common-table-name)
641 "Make a set of data tables."
642 (cli:with-options (host port database (user "") (password "") use-ssl
643 log-dir)
644 (launch-logger log-dir)
645 (with-connection (list database user password host :port port
646 :use-ssl (s-sql:from-sql-name use-ssl))
647 (muffle-postgresql-warnings)
648 (create-acquisition-project common-table-name))
649 (cl-log:log-message
650 :db-dat
651 "Created a fresh acquisition project by the name of ~A ~
652 in database ~A at ~A:~D."
653 common-table-name database host port)))
655 (defun cli:delete-acquisition-project-action (common-table-name)
656 "Delete an acquisition project."
657 (cli:with-options (host port database (user "") (password "") use-ssl
658 log-dir)
659 (launch-logger log-dir)
660 (when (yes-or-no-p
661 "You asked me to delete acquisition-project ~A ~
662 (including all its measurements) ~
663 from database ~A at ~A:~D. Proceed?"
664 common-table-name database host port)
665 (with-connection (list database user password host :port port
666 :use-ssl (s-sql:from-sql-name use-ssl))
667 (muffle-postgresql-warnings)
668 (let ((project-did-exist-p
669 (delete-acquisition-project common-table-name)))
670 (cl-log:log-message
671 :db-dat
672 "~:[Tried to delete nonexistent~;Deleted~] ~
673 acquisition project ~A from database ~A at ~A:~D."
674 project-did-exist-p common-table-name database host port))))))
676 (defun cli:delete-measurement-action (measurement-id)
677 "Delete a measurement by its measurement-id."
678 (cli:with-options (host port database (user "") (password "") use-ssl
679 log-dir)
680 (launch-logger log-dir)
681 (with-connection (list database user password host :port port
682 :use-ssl (s-sql:from-sql-name use-ssl))
683 (let ((measurement-did-exist-p
684 (delete-measurement measurement-id)))
685 (cl-log:log-message
686 :db-dat
687 "~:[Tried to delete nonexistent~;Deleted~] ~
688 measurement with ID ~A from database ~A at ~A:~D."
689 measurement-did-exist-p measurement-id database host port)))))
691 (defun cli:list-acquisition-project-action (&optional common-table-name)
692 "List content of acquisition projects."
693 (cli:with-options (host port database (user "") (password "") use-ssl)
694 (with-connection (list database user password host :port port
695 :use-ssl (s-sql:from-sql-name use-ssl))
696 (let ((content
697 (if (stringp common-table-name)
698 (query
699 (:order-by
700 (:select
701 'common-table-name
702 'sys-acquisition-project.acquisition-project-id
703 'measurement-id
704 'directory
705 'cartesian-system
706 :from
707 'sys-acquisition-project :natural :left-join 'sys-measurement
708 :where (:= 'common-table-name common-table-name))
709 'measurement-id))
710 (query
711 (:order-by
712 (:select
713 'common-table-name
714 'sys-acquisition-project.acquisition-project-id
715 'measurement-id
716 'directory
717 'cartesian-system
718 :from
719 'sys-acquisition-project :natural :left-join 'sys-measurement)
720 'common-table-name 'measurement-id)))))
721 (cli:format-table
722 *standard-output* " | " content
723 "Acquisition Project" "ID" "Meas. ID" "Directory" "Cartesian CS")))))
725 (defun cli:store-images-and-points-action (common-table-name)
726 "Put data into the data tables."
727 (cli:with-options (host port database (user "") (password "") use-ssl
728 log-dir
729 directory epsilon common-root aggregate-events)
730 (launch-logger log-dir)
731 (with-connection (list database user password host :port port
732 :use-ssl (s-sql:from-sql-name use-ssl))
733 (cl-log:log-message
734 :db-dat
735 "Start: storing data from ~A into acquisition project ~A ~
736 in database ~A at ~A:~D."
737 directory common-table-name database host port)
738 (store-images-and-points common-table-name directory
739 :epsilon (read-from-string epsilon nil)
740 :root-dir common-root
741 :aggregate-events aggregate-events))
742 (cl-log:log-message
743 :db-dat
744 "Finish: storing data from ~A into acquisition project ~A ~
745 in database ~A at ~A:~D."
746 directory common-table-name database host port)))
748 (defun cli:insert-footprints-action (common-table-name)
749 "Update image footprints."
750 (cli:with-options (host port database (user "") (password "") use-ssl
751 log-dir)
752 (launch-logger log-dir)
753 (with-connection (list database user password host :port port
754 :use-ssl (s-sql:from-sql-name use-ssl))
755 (let ((number-of-updated-footprints
756 (insert-footprints common-table-name)))
757 (cl-log:log-message
758 :db-dat
759 "~:[All image footprints belonging to acquisition project ~*~A ~
760 in database ~A at ~A:~D are up to date.~
761 ~;Updated ~D image footprint~:P of acquisition project ~A ~
762 in database ~A at ~A:~D.~]"
763 (plusp number-of-updated-footprints) number-of-updated-footprints
764 common-table-name database host port)))))
766 ;;; We don't seem to have two-dimensional arrays in postmodern
767 ;;(defun cli:canonicalize-bayer-pattern (raw &optional sql-string-p)
768 ;; "Convert list of strings of comma-separated hex color strings (ex: #ff0000 for red) into an array of integers. If sql-string-p is t, convert it into a string in SQL syntax."
769 ;; (when raw
770 ;; (let* ((array
771 ;; (loop
772 ;; for row in raw
773 ;; collect
774 ;; (loop
775 ;; for hex-color in (cl-utilities:split-sequence #\, row)
776 ;; collect
777 ;; (let ((*read-base* 16))
778 ;; (assert (eql (elt hex-color 0) #\#) () "~A is not a valid color" hex-color)
779 ;; (read-from-string
780 ;; (concatenate 'string
781 ;; (subseq hex-color 5 7)
782 ;; (subseq hex-color 3 5)
783 ;; (subseq hex-color 1 3))
784 ;; nil)))))
785 ;; (rows (length array))
786 ;; (columns (length (elt array 0))))
787 ;; (if sql-string-p
788 ;; (format nil "{~{{~{~A~#^,~}}~}}" array)
789 ;; (make-array (list rows columns) :initial-contents array)))))
791 (defun cli:canonicalize-bayer-pattern (raw &optional sql-string-p)
792 "Convert a string of comma-separated hex color strings (ex: #ff0000
793 for red) into a vector of integers. If sql-string-p is t, convert it
794 into a string in SQL syntax."
795 (when raw
796 (let* ((vector
797 (loop
798 for hex-color in (cl-utilities:split-sequence #\, raw)
799 collect
800 (let ((*read-base* 16))
801 (assert (eql (elt hex-color 0) #\#)
802 () "~A is not a valid color" hex-color)
803 (read-from-string
804 (concatenate 'string
805 (subseq hex-color 5 7)
806 (subseq hex-color 3 5)
807 (subseq hex-color 1 3))
808 nil))))
809 (columns (length vector)))
810 (if sql-string-p
811 (format nil "{~{~A~#^,~}}" vector)
812 (make-array (list columns) :initial-contents vector)))))
814 (defun cli:canonicalize-color-raiser (raw &optional sql-string-p)
815 "Convert string of comma-separated numbers into a vector. If
816 sql-string-p is t, convert it into a string in SQL syntax."
817 (when raw
818 (let* ((vector
819 (loop
820 for multiplier in (cl-utilities:split-sequence #\, raw :count 3)
821 collect
822 (read-from-string multiplier nil))))
823 (if sql-string-p
824 (format nil "{~{~A~#^,~}}" vector)
825 (make-array '(3) :initial-contents vector)))))
827 (defun cli:store-stuff (store-function)
828 "Open database connection and call store-function on command line
829 options. Print return values to *standard-output*. store-function
830 should only take keyargs."
831 (let ((command-line-options
832 (cli:remaining-options)))
833 (setf (getf command-line-options :bayer-pattern)
834 (cli:canonicalize-bayer-pattern
835 (getf command-line-options :raw-bayer-pattern) t)
836 (getf command-line-options :color-raiser)
837 (cli:canonicalize-color-raiser
838 (getf command-line-options :raw-color-raiser) t))
839 (destructuring-bind (&key host port database (user "") (password "") use-ssl
840 log-dir &allow-other-keys)
841 command-line-options
842 (launch-logger log-dir)
843 (with-connection (list database user password host :port port
844 :use-ssl (s-sql:from-sql-name use-ssl))
845 (format *standard-output* "~&~{~D~#^ ~}~%"
846 (multiple-value-list
847 (apply store-function :allow-other-keys t
848 command-line-options)))))))
850 (defun cli:store-camera-hardware-action (&rest rest)
851 (declare (ignore rest))
852 (cli:store-stuff #'store-camera-hardware))
854 (defun cli:store-lens-action (&rest rest)
855 (declare (ignore rest))
856 (cli:store-stuff #'store-lens))
858 (defun cli:store-generic-device-action (&rest rest)
859 (declare (ignore rest))
860 (cli:store-stuff #'store-generic-device))
862 (defun cli:store-device-stage-of-life-action (&rest rest)
863 (declare (ignore rest))
864 (cli:store-stuff #'store-device-stage-of-life))
866 (defun cli:store-device-stage-of-life-end-action (&rest rest)
867 (declare (ignore rest))
868 (cli:store-stuff #'store-device-stage-of-life-end))
870 (defun cli:store-camera-calibration-action (&rest rest)
871 (declare (ignore rest))
872 (cli:store-stuff #'store-camera-calibration))
874 (defun cli:get-image-action (&rest rest)
875 "Output a PNG file extracted from a .pictures file; print its
876 trigger-time to stdout."
877 (declare (ignore rest))
878 (cli:with-options (count byte-position in out
879 raw-bayer-pattern raw-color-raiser)
880 (with-open-file (out-stream out :direction :output
881 :element-type 'unsigned-byte
882 :if-exists :supersede)
883 (let ((trigger-time
884 (if byte-position
885 (send-png out-stream in byte-position
886 :bayer-pattern
887 (cli:canonicalize-bayer-pattern raw-bayer-pattern)
888 :color-raiser
889 (cli:canonicalize-color-raiser raw-color-raiser))
890 (send-nth-png count out-stream in
891 :bayer-pattern
892 (cli:canonicalize-bayer-pattern raw-bayer-pattern)
893 :color-raiser
894 (cli:canonicalize-color-raiser raw-color-raiser)))))
895 (format *standard-output*
896 "~&~A~%" (timestring (utc-from-unix trigger-time)))))))
898 (defun cli:create-presentation-project-action (presentation-project-name)
899 "Make a presentation project."
900 (cli:with-options (host port database (user "") (password "") use-ssl
901 log-dir)
902 (launch-logger log-dir)
903 (with-connection (list database user password host :port port
904 :use-ssl (s-sql:from-sql-name use-ssl))
905 (muffle-postgresql-warnings)
906 (let ((fresh-project-p
907 (create-presentation-project presentation-project-name)))
908 (cl-log:log-message
909 :db-dat
910 "~:[Tried to recreate an existing~;Created a fresh~] ~
911 presentation project by the name of ~A in database ~A at ~A:~D."
912 fresh-project-p presentation-project-name database host port)))))
914 (defun cli:delete-presentation-project-action (presentation-project-name)
915 "Delete a presentation project."
916 (cli:with-options (host port database (user "") (password "") use-ssl
917 log-dir)
918 (launch-logger log-dir)
919 (when (yes-or-no-p
920 "You asked me to delete presentation-project ~A ~
921 (including its tables of user-defined points and lines, ~
922 ~A and ~A respectively) from database ~A at ~A:~D. Proceed?"
923 presentation-project-name
924 (user-point-table-name presentation-project-name)
925 (user-line-table-name presentation-project-name)
926 database host port)
927 (with-connection (list database user password host :port port
928 :use-ssl (s-sql:from-sql-name use-ssl))
929 (muffle-postgresql-warnings)
930 (let ((project-did-exist-p
931 (delete-presentation-project presentation-project-name)))
932 (cl-log:log-message
933 :db-dat
934 "~:[Tried to delete nonexistent~;Deleted~] ~
935 presentation project ~A from database ~A at ~A:~D."
936 project-did-exist-p presentation-project-name
937 database host port))))))
939 (defun cli:add-to-presentation-project-action (presentation-project-name)
940 "Add measurements to a presentation project."
941 (cli:with-options (host port database (user "") (password "") use-ssl
942 log-dir
943 measurement-id acquisition-project)
944 (launch-logger log-dir)
945 (with-connection (list database user password host :port port
946 :use-ssl (s-sql:from-sql-name use-ssl))
947 (add-to-presentation-project presentation-project-name
948 :measurement-ids measurement-id
949 :acquisition-project acquisition-project))
950 (cl-log:log-message
951 :db-dat
952 "Added ~@[measurement-ids ~{~D~#^, ~}~]~
953 ~@[all measurements from acquisition project ~A~] ~
954 to presentation project ~A in database ~A at ~A:~D."
955 measurement-id acquisition-project
956 presentation-project-name database host port)))
958 (defun cli:remove-from-presentation-project-action (presentation-project-name)
959 "Add measurements to a presentation project."
960 (cli:with-options (host port database (user "") (password "") use-ssl
961 log-dir
962 measurement-id acquisition-project)
963 (launch-logger log-dir)
964 (with-connection (list database user password host :port port
965 :use-ssl (s-sql:from-sql-name use-ssl))
966 (remove-from-presentation-project
967 presentation-project-name
968 :measurement-ids measurement-id
969 :acquisition-project acquisition-project))
970 (cl-log:log-message
971 :db-dat
972 "Removed ~@[measurement-ids ~{~D~#^, ~}~]~
973 ~@[all measurements that belong to acquisition project ~A~] ~
974 from presentation project ~A in database ~A at ~A:~D."
975 measurement-id acquisition-project
976 presentation-project-name database host port)))
978 (defun cli:redefine-trigger-function-action (presentation-project-name)
979 "Recreate an SQL trigger function that is fired on changes to the
980 user point table, and fire it once."
981 (cli:with-options (host port database (user "") (password "") use-ssl
982 log-dir
983 plpgsql-body)
984 (launch-logger log-dir)
985 (with-connection (list database user password host :port port
986 :use-ssl (s-sql:from-sql-name use-ssl))
987 (muffle-postgresql-warnings)
988 (let ((body-text
989 (make-array '(1) :adjustable t :fill-pointer 0
990 :element-type 'character)))
991 (if plpgsql-body
992 (with-open-file (stream plpgsql-body)
993 (loop
994 for c = (read-char stream nil)
995 while c
996 do (vector-push-extend c body-text))
997 (create-presentation-project-trigger-function
998 presentation-project-name
999 body-text
1000 (s-sql:to-sql-name (user-point-table-name
1001 presentation-project-name))
1002 (s-sql:to-sql-name (user-line-table-name
1003 presentation-project-name))))
1004 (create-presentation-project-trigger-function
1005 presentation-project-name))
1006 (fire-presentation-project-trigger-function presentation-project-name)
1007 (cl-log:log-message
1008 :db-dat
1009 "Defined (and fired once) ~
1010 a trigger function associatad with user point table of ~
1011 presentation project ~A in database ~A at ~A:~D to ~
1012 ~:[perform a minimal default action.~;perform the body given ~
1013 in file ~:*~A, whose content is is:~&~A~]"
1014 presentation-project-name database host port
1015 plpgsql-body body-text)))))
1017 (defun cli:create-aux-view-action (presentation-project-name)
1018 "Connect presentation project to an auxiliary data table by means of
1019 a view."
1020 (cli:with-options (host (aux-host host) port (aux-port port)
1021 database (aux-database database)
1022 (user "") (aux-user user)
1023 (password "") (aux-password password)
1024 use-ssl (aux-use-ssl use-ssl)
1025 log-dir
1026 coordinates-column numeric-column text-column
1027 aux-table)
1028 (launch-logger log-dir)
1029 (with-connection (list aux-database aux-user aux-password aux-host
1030 :port aux-port
1031 :use-ssl (s-sql:from-sql-name aux-use-ssl))
1032 (let ((aux-view-in-phoros-db-p
1033 (every #'equal
1034 (list host port database user password use-ssl)
1035 (list aux-host aux-port aux-database
1036 aux-user aux-password aux-use-ssl)))
1037 (aux-view-exists-p
1038 (aux-view-exists-p presentation-project-name)))
1039 (when (or
1040 aux-view-in-phoros-db-p
1041 (yes-or-no-p
1042 "I'm going to ~:[create~;replace~] a view named ~A ~
1043 in database ~A at ~A:~D. Proceed?"
1044 aux-view-exists-p
1045 (aux-point-view-name presentation-project-name)
1046 aux-database aux-host aux-port))
1047 (muffle-postgresql-warnings)
1048 (when aux-view-exists-p
1049 (delete-aux-view presentation-project-name))
1050 (apply #'create-aux-view
1051 presentation-project-name
1052 :coordinates-column (s-sql:to-sql-name coordinates-column)
1053 :numeric-columns numeric-column
1054 :text-columns text-column
1055 :allow-other-keys t
1056 (cli:remaining-options))
1057 (add-spherical-mercator-ref)
1058 (cl-log:log-message
1059 :db-dat
1060 "~:[Created~;Updated~] in database ~A at ~A:~D a view called ~A ~
1061 into table (of auxiliary data) ~A. Coordinates column is ~A. ~
1062 ~:[No numeric columns.~;Numeric column(s): ~:*~{~A~#^, ~}.~] ~
1063 ~:[No text columns.~;Text column(s): ~:*~{~A~#^, ~}.~] ~
1064 Also, ~0@*~:[created~;recreated~] in the same database a ~
1065 function called ~9@*~A."
1066 aux-view-exists-p
1067 aux-database aux-host aux-port
1068 (aux-point-view-name presentation-project-name)
1069 aux-table coordinates-column
1070 numeric-column text-column
1071 (thread-aux-points-function-name presentation-project-name)))))))
1073 (defun cli:store-user-points-action (presentation-project)
1074 "Store user points from a GeoJSON file into database."
1075 (cli:with-options (host port database (user "") (password "") use-ssl
1076 log-dir
1077 json-file)
1078 (launch-logger log-dir)
1079 (with-connection (list database user password host :port port
1080 :use-ssl (s-sql:from-sql-name use-ssl))
1081 (muffle-postgresql-warnings)
1082 (multiple-value-bind
1083 (points-stored points-already-in-db points-tried)
1084 (apply #'store-user-points presentation-project
1085 :allow-other-keys t
1086 (cli:remaining-options))
1087 (cl-log:log-message
1088 :db-dat
1089 "Tried to store the ~D user point~:P I found in file ~A ~
1090 into presentation project ~A in database ~A at ~A:~D. ~
1091 ~:[~:[~D~;None~*~]~;All~2*~] of them ~:[were~;was~] ~
1092 already present. ~
1093 ~:[~:[~:[~D points have~;1 point has~*~]~;Nothing has~2*~]~
1094 ~;All points tried have~3*~] ~
1095 been added to the user point table."
1096 points-tried
1097 (truename json-file)
1098 presentation-project database host port
1099 (= points-already-in-db points-tried)
1100 (zerop points-already-in-db)
1101 points-already-in-db
1102 (<= points-already-in-db 1)
1103 (= points-stored points-tried)
1104 (zerop points-stored)
1105 (= 1 points-stored)
1106 points-stored)))))
1108 (defun cli:get-user-points-action (presentation-project)
1109 "Save user points of presentation project into a GeoJSON file."
1110 (cli:with-options (host port database (user "") (password "") use-ssl
1111 log-dir
1112 json-file)
1113 (launch-logger log-dir)
1114 (with-connection (list database user password host :port port
1115 :use-ssl (s-sql:from-sql-name use-ssl))
1116 (multiple-value-bind (user-points user-point-count)
1117 (get-user-points (user-point-table-name presentation-project))
1118 (assert json-file ()
1119 "Don't know where to store. Try option --json-file")
1120 (with-open-file (stream json-file
1121 :direction :output
1122 :if-exists :supersede)
1123 (princ user-points stream))
1124 (cl-log:log-message
1125 :db-dat
1126 "Saved ~D user point~:P from presentation project ~A in ~
1127 database ~A at ~A:~D into file ~A."
1128 user-point-count
1129 presentation-project database host port
1130 (truename json-file))))))
1132 (defun cli:create-user-action (presentation-project-user)
1133 "Define a new user."
1134 (let (fresh-user-p)
1135 (cli:with-options (host port database (user "") (password "") use-ssl
1136 log-dir
1137 presentation-project
1138 user-full-name user-role)
1139 (launch-logger log-dir)
1140 (with-connection (list database user password host :port port
1141 :use-ssl (s-sql:from-sql-name use-ssl))
1142 (setf fresh-user-p
1143 (apply #'create-user
1144 presentation-project-user
1145 :allow-other-keys t
1146 :presentation-projects presentation-project
1147 (cli:remaining-options))))
1148 (cl-log:log-message
1149 :db-dat ;TODO: We're listing nonexistent p-projects here as well.
1150 "~:[Updated~;Created~] user ~A (~A) who has ~A access ~
1151 to ~:[no ~;~]presentation project(s)~:*~{ ~A~#^,~} ~
1152 in database ~A at ~A:~D."
1153 fresh-user-p presentation-project-user
1154 user-full-name user-role
1155 presentation-project database host port))))
1157 (defun cli:delete-user-action (presentation-project-user)
1158 "Delete a presentation project user."
1159 (cli:with-options (host port database (user "") (password "") use-ssl
1160 log-dir)
1161 (launch-logger log-dir)
1162 (with-connection (list database user password host :port port
1163 :use-ssl (s-sql:from-sql-name use-ssl))
1164 (let ((user-did-exist-p
1165 (delete-user presentation-project-user)))
1166 (cl-log:log-message
1167 :db-dat
1168 "~:[Tried to delete nonexistent~;Deleted~] ~
1169 presentation project user ~A from database ~A at ~A:~D."
1170 user-did-exist-p presentation-project-user database host port)))))
1172 (defun cli:list-user-action (&optional presentation-project-user)
1173 "List presentation project users together with their presentation
1174 projects."
1175 (cli:with-options (host port database (user "") (password "") use-ssl)
1176 (with-connection (list database user password host :port port
1177 :use-ssl (s-sql:from-sql-name use-ssl))
1178 (let ((content
1179 (if (stringp presentation-project-user)
1180 (query
1181 (:order-by
1182 (:select
1183 'user-name 'sys-user.user-id 'user-password
1184 'user-full-name 'presentation-project-name
1185 'sys-user-role.presentation-project-id 'user-role
1186 :from 'sys-user 'sys-user-role 'sys-presentation-project
1187 :where (:and (:= 'sys-user-role.presentation-project-id
1188 'sys-presentation-project.presentation-project-id)
1189 (:= 'sys-user.user-id 'sys-user-role.user-id)
1190 (:= 'user-name presentation-project-user)))
1191 'user-name))
1192 (query
1193 (:order-by
1194 (:select
1195 'user-name 'sys-user.user-id 'user-password
1196 'user-full-name 'presentation-project-name
1197 'sys-user-role.presentation-project-id 'user-role
1198 :from 'sys-user 'sys-user-role 'sys-presentation-project
1199 :where (:and (:= 'sys-user-role.presentation-project-id
1200 'sys-presentation-project.presentation-project-id)
1201 (:= 'sys-user.user-id 'sys-user-role.user-id)))
1202 'user-name)))))
1203 (cli:format-table
1204 *standard-output* " | " content
1205 "User" "ID" "Password" "Full Name" "Presentation Project" "ID" "Role")))))
1207 (defun cli:list-presentation-project-action (&optional presentation-project)
1208 "List content of presentation projects."
1209 (cli:with-options (host port database (user "") (password "") use-ssl)
1210 (with-connection (list database user password host :port port
1211 :use-ssl (s-sql:from-sql-name use-ssl))
1212 (let ((content
1213 (if (stringp presentation-project)
1214 (query
1215 (:order-by
1216 (:select
1217 'presentation-project-name
1218 'sys-presentation-project.presentation-project-id
1219 'sys-presentation.measurement-id
1220 'common-table-name
1221 'sys-measurement.acquisition-project-id
1222 :from
1223 'sys-presentation-project 'sys-presentation
1224 'sys-measurement 'sys-acquisition-project
1225 :where
1226 (:and (:= 'sys-presentation-project.presentation-project-id
1227 'sys-presentation.presentation-project-id)
1228 (:= 'sys-presentation.measurement-id
1229 'sys-measurement.measurement-id)
1230 (:= 'sys-measurement.acquisition-project-id
1231 'sys-acquisition-project.acquisition-project-id)
1232 (:= 'presentation-project-name
1233 presentation-project)))
1234 'presentation-project-name
1235 'sys-presentation.measurement-id))
1236 (query
1237 (:order-by
1238 (:select
1239 'presentation-project-name
1240 'sys-presentation-project.presentation-project-id
1241 'sys-presentation.measurement-id
1242 'common-table-name
1243 'sys-measurement.acquisition-project-id
1244 :from
1245 'sys-presentation-project 'sys-presentation
1246 'sys-measurement 'sys-acquisition-project
1247 :where
1248 (:and (:= 'sys-presentation-project.presentation-project-id
1249 'sys-presentation.presentation-project-id)
1250 (:= 'sys-presentation.measurement-id
1251 'sys-measurement.measurement-id)
1252 (:= 'sys-measurement.acquisition-project-id
1253 'sys-acquisition-project.acquisition-project-id)))
1254 'presentation-project-name
1255 'sys-presentation.measurement-id)))))
1256 (cli:format-table
1257 *standard-output* " | " content
1258 "Presentation Project" "ID" "Meas. ID" "Acquisition Project" "ID")))))
1260 (defun cli:format-table (destination column-separator content
1261 &rest column-headers)
1262 "Print content (a list of lists) to destination."
1263 (let* ((rows
1264 (append (list column-headers) (list ()) content))
1265 (number-of-rows (length column-headers))
1266 (widths
1267 (loop
1268 for column from 0 below number-of-rows collect
1269 (loop
1270 for row in rows
1271 maximize (length (format nil "~A" (nth column row)))))))
1272 (setf (second rows)
1273 (loop
1274 for width in widths collect
1275 (make-string width :initial-element #\-)))
1276 (loop
1277 for row in rows do
1278 (format destination "~&~{~VA~1,#^~A~}~%"
1279 (loop
1280 for width in widths and field in row
1281 collect width collect field collect column-separator)))))
1283 (defun cli:server-action (&rest rest)
1284 "Start the HTTP server."
1285 (declare (ignore rest))
1286 (cli:with-options (host (aux-host host) port (aux-port port)
1287 database (aux-database database)
1288 (user "") (aux-user user)
1289 (password "") (aux-password password)
1290 use-ssl (aux-use-ssl use-ssl)
1291 log-dir
1292 http-port address common-root)
1293 (launch-logger log-dir)
1294 (setf *postgresql-credentials*
1295 (list database user password host :port port
1296 :use-ssl (s-sql:from-sql-name use-ssl)))
1297 (setf *postgresql-aux-credentials*
1298 (list aux-database aux-user aux-password aux-host :port aux-port
1299 :use-ssl (s-sql:from-sql-name aux-use-ssl)))
1300 (insert-all-footprints *postgresql-credentials*)
1301 (start-server :http-port http-port :address address
1302 :common-root common-root)
1303 (cl-log:log-message
1304 :info
1305 "HTTP server listens on port ~D ~
1306 of ~:[all available addresses~;address ~:*~A~]. ~
1307 Phoros database is ~A on ~A:~D. Auxiliary database is ~A on ~A:~D. ~
1308 Files are searched for in ~A."
1309 http-port address
1310 database host port
1311 aux-database aux-host aux-port
1312 common-root)
1313 (loop (sleep 10))))