1 ;;; PHOROS -- Photogrammetric Road Survey
2 ;;; Copyright (C) 2010, 2011, 2012 Bert Burgemeister
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.
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.
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.
20 (setf *js-target-version
* 1.8)
22 ;;; Debug helpers. TODO: remove them.
23 (defparameter *t
* nil
)
24 (defparameter *tt
* nil
)
26 (cffi:define-foreign-library phoml
27 (:unix
(:or
"./libphoml.so"
28 "./phoml/lib/libphoml.so"))
29 (t (:default
"libphoml")))
31 (defparameter *standard-coordinates
* 4326
32 "EPSG code of the coordinate system that we use for communication.")
34 (defparameter *spherical-mercator
* 900913
35 "EPSG code of the coordinate system used for some distance calculations.")
37 (defvar *postgresql-credentials
* nil
38 "A list: (database user password host &key (port 5432) use-ssl).")
40 (defvar *postgresql-aux-credentials
* nil
41 "A list: (database user password host &key (port 5432) use-ssl).")
43 (defparameter *photogrammetry-mutex
* (bt:make-lock
"photogrammetry"))
45 (setf *read-default-float-format
* 'double-float
)
47 (defparameter *phoros-server
* nil
"Hunchentoot acceptor.")
49 (defparameter *common-root
* nil
50 "Root directory; contains directories of measuring data.")
52 (defparameter *proxy-root
* "phoros"
53 "First directory element of the server URL. Must correspond to the
54 proxy configuration if Phoros is hidden behind a proxy.")
56 (defparameter *login-intro
* nil
57 "A few friendly words to be shown below the login form.")
59 (defparameter *number-of-images
* 4
60 "Number of photos shown to the HTTP client.")
62 (defparameter *aux-numeric-labels
* nil
63 "Labels for auxiliary numeric data rows shown to the HTTP client.")
65 (defparameter *aux-text-labels
* nil
66 "Labels for auxiliary text data rows shown to the HTTP client.")
68 (defparameter *browser-cache-max-age
* (* 3600 24 7)
69 "Value x for Cache-Control:max-age=x, for images on client.")
71 (defparameter *number-of-features-per-layer
* 500
72 "What we think a browser can swallow.")
74 (defparameter *number-of-points-per-aux-linestring
* 500
75 "What we think a browser can swallow.")
77 (defparameter *user-point-creation-date-format
* "IYYY-MM-DD HH24:MI:SS TZ"
78 "SQL date format used for display and GeoJSON export of user points.")
80 (defparameter *phoros-version
*
81 (asdf:component-version
(asdf:find-system
:phoros
))
82 "Phoros version as defined in system definition.")
84 (defparameter *aggregate-view-columns
*
86 'recorded-device-id
;debug
87 'device-stage-of-life-id
;debug
88 'generic-device-id
;debug
91 'filename
'byte-position
'point-id
93 ;;'coordinates ;the search target
94 'longitude
'latitude
'ellipsoid-height
96 'east-sd
'north-sd
'height-sd
98 'roll-sd
'pitch-sd
'heading-sd
99 'sensor-width-pix
'sensor-height-pix
101 'bayer-pattern
'color-raiser
103 'dx
'dy
'dz
'omega
'phi
'kappa
104 'c
'xh
'yh
'a1
'a2
'a3
'b1
'b2
'c1
'c2
'r0
105 'b-dx
'b-dy
'b-dz
'b-rotx
'b-roty
'b-rotz
107 'b-drotx
'b-droty
'b-drotz
)
108 "Most of the column names of aggregate-view.")
110 (defun version-number-parts (dotted-string)
111 "Return the three version number components of something like
114 (values-list (mapcar #'parse-integer
115 (cl-utilities:split-sequence
#\. dotted-string
)))))
117 (defun phoros-version (&key major minor revision
)
118 "Return version of this program, either one integer part as denoted by
119 the key argument, or the whole dotted string."
120 (multiple-value-bind (major-number minor-number revision-number
)
121 (version-number-parts *phoros-version
*)
122 (cond (major major-number
)
124 (revision revision-number
)
125 (t *phoros-version
*))))
127 (defun check-dependencies ()
128 "Say OK if the necessary external dependencies are available."
131 (geographic-to-utm 33 13 52) ;check cs2cs
132 (phoros-photogrammetry:del-all
) ;check photogrammetry
133 (initialize-leap-seconds) ;check source of leap second info
134 (format *error-output
* "~&OK~%"))
135 (error (e) (format *error-output
* "~A~&" e
))))
137 (defun muffle-postgresql-warnings ()
138 "For current DB, silence PostgreSQL's warnings about implicitly
140 (unless (cli:verbosity-level
:postgresql-warnings
)
141 (execute "SET client_min_messages TO ERROR;")))
143 (defun check-db (db-credentials)
144 "Check postgresql connection. Return t if successful; show error on
145 *error-output* otherwise. db-credentials is a list like so: (database
146 user password host &key (port 5432) use-ssl)."
149 (setf connection
(apply #'connect db-credentials
))
150 (error (e) (format *error-output
* "Database connection ~S failed: ~A~&"
153 (disconnect connection
)
156 (defun ignore-warnings (c) (declare (ignore c
)) (muffle-warning))
158 (defmethod hunchentoot:session-cookie-name
(acceptor)
159 (declare (ignore acceptor
))
162 (defun start-server (&key
(proxy-root "phoros") (http-port 8080) address
164 "Start the presentation project server which listens on http-port
165 at address. Address defaults to all addresses of the local machine."
166 (setf *phoros-server
*
167 (make-instance 'hunchentoot
:easy-acceptor
170 :document-root
(ensure-directories-exist
172 :error-template-directory
(ensure-directories-exist
173 "unexpected_html/errors/")))
174 (setf hunchentoot
:*session-max-time
* (* 3600 24))
175 (setf *proxy-root
* proxy-root
)
176 (setf *common-root
* common-root
)
177 (check-db *postgresql-credentials
*)
178 (with-connection *postgresql-credentials
*
179 (assert-phoros-db-major-version))
180 (hunchentoot:reset-session-secret
)
181 (hunchentoot:start
*phoros-server
*))
183 (defun stop-server () (hunchentoot:stop
*phoros-server
*))
185 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
186 (register-sql-operators :2+-ary
:&& :overlaps
))
188 (hunchentoot:define-easy-handler phoros-handler
()
189 "First HTTP contact: if necessary, check credentials, establish new
191 (with-connection *postgresql-credentials
*
192 (let* ((s (cl-utilities:split-sequence
194 (hunchentoot:script-name
*)
195 :remove-empty-subseqs t
))
196 (presentation-project-name (second s
))
197 (presentation-project-id
199 (presentation-project-id-from-name presentation-project-name
))))
201 ;; TODO: remove the following line (which seems to function as a
202 ;; wakeup call of sorts)...
203 (get-dao 'sys-user-role
0 0)
204 ;; ...and make sure the following error doesn't occur any longer
205 ;; while accessing the HTTP server:
206 ;; #<POSTMODERN:DAO-CLASS PHOROS::SYS-USER-ROLE> cannot be printed readably.
209 ((null presentation-project-id
)
210 (setf (hunchentoot:return-code
*) hunchentoot
:+http-not-found
+))
211 ((and (equal (hunchentoot:session-value
'presentation-project-name
)
212 presentation-project-name
)
213 (hunchentoot:session-value
'authenticated-p
))
214 (hunchentoot:redirect
215 (format nil
"/~A/lib/view-~A"
222 (setf (hunchentoot:session-value
'presentation-project-name
)
223 presentation-project-name
)
224 (setf (hunchentoot:session-value
'presentation-project-id
)
225 presentation-project-id
)
226 (setf (hunchentoot:session-value
'presentation-project-bbox
)
229 (bounding-box (get-dao 'sys-presentation-project
230 presentation-project-name
)))))
231 (if (or (null bbox
) (eq :null bbox
))
234 (setf (hunchentoot:session-value
'aux-data-p
)
235 (with-connection *postgresql-aux-credentials
*
236 (view-exists-p (aux-point-view-name
237 presentation-project-name
))))
238 (setf (hunchentoot:session-value
'number-of-threads
) 0)
239 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
241 :style
"font-family:sans-serif;"
243 :method
"post" :enctype
"multipart/form-data"
244 :action
(format nil
"/~A/lib/authenticate"
248 (:legend
(:b
(:a
:href
"http://phoros.boundp.org"
249 :style
"text-decoration:none;"
251 (who:fmt
" [~A]" presentation-project-name
)))
253 (:b
(:em
"You can't do much without JavaScript there.")))
256 (:input
:type
"text" :name
"user-name"))
259 (:input
:type
"password" :name
"user-password")
261 (:span
:id
"cackle"))
262 (:input
:type
"submit" :value
"Submit"
264 (setf (chain document
265 (get-element-by-id "cackle")
267 "Ok, let's see…"))))
268 (:script
:type
"text/javascript"
269 (who:str
(ps (chain document
274 for i in
*login-intro
*
275 do
(who:htm
(:p
(who:str i
))))))))))))
277 (pushnew (hunchentoot:create-regex-dispatcher
"/phoros/(?!lib/)"
279 hunchentoot
:*dispatch-table
*)
281 (defun stored-bbox ()
282 "Return stored bounding box for user and presentation project of
284 (with-connection *postgresql-credentials
*
285 (let ((bbox (bounding-box
286 (get-dao 'sys-user-role
287 (hunchentoot:session-value
289 (hunchentoot:session-value
290 'presentation-project-id
)))))
292 (hunchentoot:session-value
'presentation-project-bbox
)
295 (defun stored-cursor ()
296 "Return stored cursor position for user and presentation project of
298 (with-connection *postgresql-credentials
*
301 (:select
(:st_x
'cursor
) (:st_y
'cursor
)
303 :where
(:and
(:= 'user-id
304 (hunchentoot:session-value
'user-id
))
305 (:= 'presentation-project-id
306 (hunchentoot:session-value
307 'presentation-project-id
))
308 (:raw
"cursor IS NOT NULL")))
311 (format nil
"~{~F~#^,~}" cursor
)))))
314 (hunchentoot:define-easy-handler
315 (authenticate-handler :uri
"/phoros/lib/authenticate"
316 :default-request-type
:post
)
318 "Check user credentials."
319 (with-connection *postgresql-credentials
*
320 (let* ((user-name (hunchentoot:post-parameter
"user-name"))
321 (user-password (hunchentoot:post-parameter
"user-password"))
322 (presentation-project-id (hunchentoot:session-value
323 'presentation-project-id
))
325 (when presentation-project-id
328 'sys-user.user-full-name
330 'sys-user-role.user-role
331 :from
'sys-user-role
'sys-user
333 (:= 'presentation-project-id presentation-project-id
)
334 (:= 'sys-user-role.user-id
'sys-user.user-id
)
335 (:= 'user-name user-name
)
336 (:= 'user-password user-password
)))
338 (user-full-name (first user-info
))
339 (user-id (second user-info
))
340 (user-role (third user-info
)))
343 (setf (hunchentoot:session-value
'authenticated-p
) t
344 (hunchentoot:session-value
'user-name
) user-name
345 (hunchentoot:session-value
'user-full-name
) user-full-name
346 (hunchentoot:session-value
'user-id
) user-id
347 (hunchentoot:session-value
'user-role
) user-role
)
348 (hunchentoot:redirect
349 (format nil
"/~A/lib/view-~A"
354 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
356 :style
"font-family:sans-serif;"
358 (:a
:href
(format nil
"/~A/~A/"
360 (hunchentoot:session-value
361 'presentation-project-name
))
364 (defun assert-authentication ()
365 "Abort request handler on unauthorized access."
366 (unless (hunchentoot:session-value
'authenticated-p
)
367 (setf (hunchentoot:return-code
*) hunchentoot
:+http-precondition-failed
+)
368 (hunchentoot:abort-request-handler
)))
370 (hunchentoot:define-easy-handler logout-handler
(bbox longitude latitude
)
371 (if (hunchentoot:session-value
'authenticated-p
)
372 (with-connection *postgresql-credentials
*
373 (let ((presentation-project-name
374 (hunchentoot:session-value
'presentation-project-name
))
376 (get-dao 'sys-user-role
377 (hunchentoot:session-value
'user-id
)
378 (hunchentoot:session-value
'presentation-project-id
))))
381 (setf (bounding-box sys-user-role
) bbox
))
382 (when (and longitude latitude
)
383 (let* ;; kludge: should be done by some library, not by DB query
384 ((point-form (format nil
"POINT(~F ~F)" longitude latitude
))
385 (point-wkb (query (:select
386 (:st_geomfromtext point-form
))
388 (setf (cursor sys-user-role
) point-wkb
)))
389 (update-dao sys-user-role
))
390 (hunchentoot:remove-session hunchentoot
:*session
*)
391 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
397 "Phoros: logged out" )))
398 (:link
:rel
"stylesheet"
399 :href
(format nil
"/~A/lib/css-~A/style.css"
404 (:h1
:id
"title" "Phoros: logged out")
405 (:p
"Log back in to project "
406 (:a
:href
(format nil
"/~A/~A"
408 presentation-project-name
)
409 (who:fmt
"~A." presentation-project-name
))))))))
412 (pushnew (hunchentoot:create-regex-dispatcher
"/logout" 'logout-handler
)
413 hunchentoot
:*dispatch-table
*)
415 (define-condition superseded
() ()
417 "Tell a thread to finish as soon as possible taking any shortcuts
420 (hunchentoot:define-easy-handler
421 (selectable-restrictions :uri
"/phoros/lib/selectable-restrictions.json"
422 :default-request-type
:post
)
424 "Respond with a list of restrictions the user may choose from."
425 (assert-authentication)
426 (setf (hunchentoot:content-type
*) "application/json")
427 (with-connection *postgresql-credentials
*
428 (json:encode-json-to-string
431 (:select
'restriction-id
432 :from
'sys-selectable-restriction
433 :where
(:= 'presentation-project-id
434 (hunchentoot:session-value
435 'presentation-project-id
)))
439 (defun selected-restrictions (presentation-project-id selected-restriction-ids
)
440 "Get from current database connection a list of restriction clauses
441 belonging to presentation-project-id and ids from list
442 selected-restriction-ids."
445 `(:select
'sql-clause
446 :from
'sys-selectable-restriction
447 :where
(:and
(:= 'presentation-project-id
448 ,presentation-project-id
)
450 ,@(loop for i in selected-restriction-ids
451 collect
(list := 'restriction-id i
))))))
454 (defun sql-where-conjunction (sql-boolean-clauses)
455 "Parenthesize sql-boolean-clauses and concatenate them into a
456 string, separated by \"AND\". Return \" TRUE \" if
457 sql-boolean-clauses is nil."
458 (if sql-boolean-clauses
459 (apply #'concatenate
'string
(butlast (loop
460 for i in sql-boolean-clauses
467 (hunchentoot:define-easy-handler
468 (nearest-image-data :uri
"/phoros/lib/nearest-image-data"
469 :default-request-type
:post
)
471 "Receive coordinates, respond with the count nearest json objects
472 containing picture url, calibration parameters, and car position,
473 wrapped in an array. Wipe away any unfinished business first."
474 (assert-authentication)
475 (dolist (old-thread (hunchentoot:session-value
'recent-threads
))
477 (bt:interrupt-thread old-thread
478 #'(lambda () (signal 'superseded
)))))
479 (setf (hunchentoot:session-value
'recent-threads
) nil
)
480 (setf (hunchentoot:session-value
'number-of-threads
) 1)
481 (push (bt:current-thread
) (hunchentoot:session-value
'recent-threads
))
482 (setf (hunchentoot:content-type
*) "application/json")
483 (with-connection *postgresql-credentials
*
484 (let* ((presentation-project-id (hunchentoot:session-value
485 'presentation-project-id
))
486 (common-table-names (common-table-names
487 presentation-project-id
))
488 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
489 (longitude (cdr (assoc :longitude data
)))
490 (latitude (cdr (assoc :latitude data
)))
491 (count (cdr (assoc :count data
)))
492 (zoom (cdr (assoc :zoom data
)))
493 (snap-distance ;bogus distance in degrees,
494 (* 100e-5 ; assuming geographic
495 (expt 2 (- ; coordinates
496 14 ; (1m = 1e-5 degrees)
499 (point-form (format nil
"POINT(~F ~F)" longitude latitude
))
500 (selected-restrictions-conjunction
501 (sql-where-conjunction
502 (selected-restrictions presentation-project-id
503 (cdr (assoc :selected-restriction-ids
505 (nearest-footprint-centroid-query
506 ;; Inserting the following into
507 ;; image-data-with-footprints-query as a subquery would
508 ;; work correctly but is way too slow.
513 ,@*aggregate-view-columns
*
519 for common-table-name
520 in common-table-names
521 for aggregate-view-name
522 = (aggregate-view-name
528 (:st_centroid
'footprint
)
531 ,*standard-coordinates
*))
533 (:as
(:st_centroid
'footprint
)
535 ,@*aggregate-view-columns
*
539 ;; no-ops wrt self-references in
540 ;; selected-restrictions-conjunction
541 ,@(postmodern-as-clauses
542 (pairlis *aggregate-view-columns
*
543 *aggregate-view-columns
*))
544 :from
',aggregate-view-name
)
545 'images-of-acquisition-project
)
548 (:= 'presentation-project-id
549 ,presentation-project-id
)
554 ,*standard-coordinates
*)
556 (:raw
,selected-restrictions-conjunction
)))))
560 (nearest-footprint-image
561 (ignore-errors (logged-query "centroid of nearest footprint"
562 nearest-footprint-centroid-query
564 (nearest-footprint-centroid
565 (cdr (assoc :centroid nearest-footprint-image
)))
566 (image-data-with-footprints-query
572 for common-table-name in common-table-names
573 for aggregate-view-name
574 = (aggregate-view-name common-table-name
)
577 ,@*aggregate-view-columns
*
580 ((:is-null
'footprint
) 'coordinates
)
581 (t (:st_centroid
'footprint
)))
582 ,nearest-footprint-centroid
)
584 (:as
(:not
(:is-null
'footprint
))
586 ,(when (cli:verbosity-level
:render-footprints
)
587 '(:as
(:st_asewkt
'footprint
)
592 ,@(postmodern-as-clauses
593 nearest-footprint-image
)
594 :from
',aggregate-view-name
)
595 'images-of-acquisition-project-plus-reference-image
)
598 (:= 'presentation-project-id
599 ,presentation-project-id
)
600 (:st_contains
'footprint
601 ,nearest-footprint-centroid
)
602 (:raw
,selected-restrictions-conjunction
)))))
605 (nearest-image-without-footprints-query
611 for common-table-name in common-table-names
612 for aggregate-view-name
613 = (aggregate-view-name common-table-name
)
616 ,@*aggregate-view-columns
*
617 (:as
(:st_distance
'coordinates
620 ,*standard-coordinates
*))
622 (:as
(:not
(:is-null
'footprint
))
627 ;; no-ops wrt self-references in
628 ;; selected-restrictions-conjunction
629 ,@(postmodern-as-clauses
630 (pairlis *aggregate-view-columns
*
631 *aggregate-view-columns
*))
632 :from
',aggregate-view-name
)
633 'images-of-acquisition-project
)
635 (:and
(:= 'presentation-project-id
636 ,presentation-project-id
)
637 (:st_dwithin
'coordinates
640 ,*standard-coordinates
*)
642 (:raw
,selected-restrictions-conjunction
)))))
645 (nearest-image-without-footprint
646 (unless nearest-footprint-centroid
;otherwise save time
647 (ignore-errors (logged-query "no footprint, first image"
648 nearest-image-without-footprints-query
650 (image-data-without-footprints-query
656 for common-table-name in common-table-names
657 for aggregate-view-name
658 = (aggregate-view-name common-table-name
)
661 ,@*aggregate-view-columns
*
662 (:as
(:st_distance
'coordinates
665 ,*standard-coordinates
*))
667 (:as
(:not
(:is-null
'footprint
))
672 ,@(postmodern-as-clauses
673 nearest-image-without-footprint
)
674 :from
',aggregate-view-name
)
675 'images-of-acquisition-project
)
677 (:and
(:= 'presentation-project-id
678 ,presentation-project-id
)
679 (:st_dwithin
'coordinates
682 ,*standard-coordinates
*)
684 (:raw
,selected-restrictions-conjunction
)))))
690 (if nearest-footprint-centroid
691 (logged-query "footprints are ready"
692 image-data-with-footprints-query
694 (logged-query "no footprints yet"
695 image-data-without-footprints-query
697 (superseded () nil
))))
698 (when (cli:verbosity-level
:render-footprints
)
702 for photo-parameter-set in result
703 for footprint-vertices
= ;something like this:
704 ;; "SRID=4326;POLYGON((14.334342229 51.723293508 118.492667334,14.334386877 51.723294417 118.404764286,14.334347429 51.72327914 118.506316418,14.334383211 51.723279895 118.435823396,14.334342229 51.723293508 118.492667334))"
705 (ignore-errors ;probably no :footprint-wkt
708 (parse-number:parse-real-number x
))
709 (cl-utilities:split-sequence
#\Space p
)))
711 (cl-utilities:split-sequence-if
716 (cdr (assoc :footprint-wkt photo-parameter-set
)))
719 (if footprint-vertices
723 '(:type
:coordinates
)
727 for footprint-vertex in footprint-vertices
728 for reprojected-vertex
=
731 ;; KLUDGE: translate keys, e.g. a1 -> a_1
732 (json:decode-json-from-string
733 (json:encode-json-to-string photo-parameter-set
))
734 (pairlis '(:x-global
:y-global
:z-global
)
736 (list (proj:degrees-to-radians
737 (first footprint-vertex
))
738 (proj:degrees-to-radians
739 (second footprint-vertex
))
740 (third footprint-vertex
))
742 (cdr (assoc :cartesian-system
743 photo-parameter-set
)))))
745 (list (cdr (assoc :m reprojected-vertex
))
746 (cdr (assoc :n reprojected-vertex
))))))
748 photo-parameter-set
))))
749 (decf (hunchentoot:session-value
'number-of-threads
))
750 (json:encode-json-to-string result
))))
752 (hunchentoot:define-easy-handler
753 (nearest-image-urls :uri
"/phoros/lib/nearest-image-urls"
754 :default-request-type
:post
)
756 "Receive coordinates, respond with a json array of the necessary
757 ingredients for the URLs of the 256 nearest images."
758 (assert-authentication)
759 (when (cli:verbosity-level
:suppress-preemptive-caching
)
760 (return-from nearest-image-urls
""))
761 (push (bt:current-thread
) (hunchentoot:session-value
'recent-threads
))
762 (if (<= (hunchentoot:session-value
'number-of-threads
)
763 0) ;only stuff cache if everything else is done
765 (incf (hunchentoot:session-value
'number-of-threads
))
766 (setf (hunchentoot:content-type
*) "application/json")
767 (with-connection *postgresql-credentials
*
768 (let* ((presentation-project-id (hunchentoot:session-value
769 'presentation-project-id
))
770 (common-table-names (common-table-names
771 presentation-project-id
))
772 (data (json:decode-json-from-string
773 (hunchentoot:raw-post-data
)))
774 (longitude (cdr (assoc :longitude data
)))
775 (latitude (cdr (assoc :latitude data
)))
777 (radius (* 5d-4
)) ; assuming geographic coordinates
778 (point-form (format nil
"POINT(~F ~F)" longitude latitude
))
786 'directory
'filename
'byte-position
787 'bayer-pattern
'color-raiser
'mounting-angle
793 for common-table-name
794 in common-table-names
795 for aggregate-view-name
796 = (aggregate-view-name common-table-name
)
800 'filename
'byte-position
801 'bayer-pattern
'color-raiser
807 ,*standard-coordinates
*))
810 ',aggregate-view-name
812 (:and
(:= 'presentation-project-id
813 ,presentation-project-id
)
818 ,*standard-coordinates
*)
825 (setf (hunchentoot:return-code
*)
826 hunchentoot
:+http-gateway-time-out
+)
828 (decf (hunchentoot:session-value
'number-of-threads
))
829 (json:encode-json-to-string result
))))
830 (setf (hunchentoot:return-code
*) hunchentoot
:+http-gateway-time-out
+)))
832 (hunchentoot:define-easy-handler
833 (store-point :uri
"/phoros/lib/store-point" :default-request-type
:post
)
835 "Receive point sent by user; store it into database."
836 (assert-authentication)
837 (let* ((presentation-project-name (hunchentoot:session-value
838 'presentation-project-name
))
839 (user-id (hunchentoot:session-value
'user-id
))
840 (user-role (hunchentoot:session-value
'user-role
))
841 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
842 (longitude (cdr (assoc :longitude data
)))
843 (latitude (cdr (assoc :latitude data
)))
844 (ellipsoid-height (cdr (assoc :ellipsoid-height data
)))
845 ;; (stdx-global (cdr (assoc :stdx-global data)))
846 ;; (stdy-global (cdr (assoc :stdy-global data)))
847 ;; (stdz-global (cdr (assoc :stdz-global data)))
848 (input-size (cdr (assoc :input-size data
)))
849 (kind (cdr (assoc :kind data
)))
850 (description (cdr (assoc :description data
)))
851 (numeric-description (cdr (assoc :numeric-description data
)))
853 (format nil
"SRID=4326; POINT(~S ~S ~S)"
854 longitude latitude ellipsoid-height
))
855 (aux-numeric-raw (cdr (assoc :aux-numeric data
)))
856 (aux-text-raw (cdr (assoc :aux-text data
)))
857 (aux-numeric (if aux-numeric-raw
858 (nullify-nil (apply #'vector aux-numeric-raw
))
860 (aux-text (if aux-text-raw
861 (nullify-nil (apply #'vector aux-text-raw
))
863 (user-point-table-name
864 (user-point-table-name presentation-project-name
)))
866 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
867 () "No write permission.")
868 (with-connection *postgresql-credentials
*
870 (= 1 (execute (:insert-into user-point-table-name
:set
873 'description description
874 'numeric-description numeric-description
875 'creation-date
'current-timestamp
876 'coordinates
(:st_geomfromewkt point-form
)
877 ;; 'stdx-global stdx-global
878 ;; 'stdy-global stdy-global
879 ;; 'stdz-global stdz-global
880 'input-size input-size
881 'aux-numeric aux-numeric
882 'aux-text aux-text
)))
883 () "No point stored. This should not happen."))))
885 (hunchentoot:define-easy-handler
886 (update-point :uri
"/phoros/lib/update-point" :default-request-type
:post
)
888 "Update point sent by user in database."
889 (assert-authentication)
890 (let* ((presentation-project-name (hunchentoot:session-value
891 'presentation-project-name
))
892 (user-id (hunchentoot:session-value
'user-id
))
893 (user-role (hunchentoot:session-value
'user-role
))
894 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
895 (user-point-id (cdr (assoc :user-point-id data
)))
896 (kind (cdr (assoc :kind data
)))
897 (description (cdr (assoc :description data
)))
898 (numeric-description (cdr (assoc :numeric-description data
)))
899 (user-point-table-name
900 (user-point-table-name presentation-project-name
)))
902 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
903 () "No write permission.")
904 (with-connection *postgresql-credentials
*
907 (:update user-point-table-name
:set
910 'description description
911 'numeric-description numeric-description
912 'creation-date
'current-timestamp
913 :where
(:and
(:= 'user-point-id user-point-id
)
914 (:or
(:= (if (string-equal user-role
925 () "No point stored. Did you try to update someone else's point ~
926 without having admin permission?"))))
928 (defun increment-numeric-string (text)
929 "Increment rightmost numeric part of text if any; otherwise append a
930 three-digit numeric part."
931 (let* ((end-of-number
932 (1+ (or (position-if #'digit-char-p text
:from-end t
)
933 (1- (length text
)))))
935 (1+ (or (position-if-not #'digit-char-p text
:from-end t
938 (width-of-number (- end-of-number start-of-number
))
939 (prefix-text (subseq text
0 start-of-number
))
940 (suffix-text (subseq text end-of-number
)))
941 (when (zerop width-of-number
)
942 (setf width-of-number
3))
943 (format nil
"~A~V,'0D~A"
946 (1+ (or (ignore-errors
949 :start start-of-number
:end end-of-number
))
953 (hunchentoot:define-easy-handler
954 (uniquify-point-attributes :uri
"/phoros/lib/uniquify-point-attributes"
955 :default-request-type
:post
)
957 "Check if received set of point-attributes are unique. If so,
958 return null; otherwise return (as a suggestion) a uniquified version
959 of point-attributes by modifying element numeric-description."
960 (assert-authentication)
961 (setf (hunchentoot:content-type
*) "application/json")
962 (let* ((presentation-project-name (hunchentoot:session-value
963 'presentation-project-name
))
964 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
965 (user-point-id (cdr (assoc :user-point-id data
)))
966 (kind (cdr (assoc :kind data
)))
967 (description (cdr (assoc :description data
)))
968 (numeric-description (cdr (assoc :numeric-description data
)))
969 (user-point-table-name
970 (user-point-table-name presentation-project-name
)))
971 (flet ((uniquep (user-point-id kind description numeric-description
)
972 "Check if given set of user-point attributes will be
981 :from user-point-table-name
982 :where
(:and
(:!= 'user-point-id user-point-id
)
984 (:= 'description description
)
985 (:= 'numeric-description
986 numeric-description
)))))
993 :from user-point-table-name
994 :where
(:and
(:= 'kind kind
)
995 (:= 'description description
)
996 (:= 'numeric-description
997 numeric-description
)))))
999 (with-connection *postgresql-credentials
*
1000 (json:encode-json-to-string
1002 user-point-id kind description numeric-description
)
1004 for s
= numeric-description
1005 then
(increment-numeric-string s
)
1006 until
(uniquep user-point-id kind description s
)
1008 (setf (cdr (assoc :numeric-description data
))
1010 (return data
))))))))
1012 (hunchentoot:define-easy-handler
1013 (delete-point :uri
"/phoros/lib/delete-point" :default-request-type
:post
)
1015 "Delete user point if user is allowed to do so."
1016 (assert-authentication)
1017 (let* ((presentation-project-name (hunchentoot:session-value
1018 'presentation-project-name
))
1019 (user-id (hunchentoot:session-value
'user-id
))
1020 (user-role (hunchentoot:session-value
'user-role
))
1021 (user-point-table-name
1022 (user-point-table-name presentation-project-name
))
1023 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
1024 (with-connection *postgresql-credentials
*
1026 (eql 1 (cond ((string-equal user-role
"admin")
1027 (execute (:delete-from user-point-table-name
1028 :where
(:= 'user-point-id data
))))
1029 ((string-equal user-role
"write")
1032 user-point-table-name
1034 (:= 'user-point-id data
)
1035 (:or
(:= 'user-id user-id
)
1042 () "No point deleted. This should not happen."))))
1044 (defun common-table-names (presentation-project-id)
1045 "Return a list of common-table-names of table sets that contain data
1046 of presentation project with presentation-project-id."
1049 (:select
'common-table-name
1051 :from
'sys-presentation
'sys-measurement
'sys-acquisition-project
1053 (:= 'sys-presentation.presentation-project-id
1054 presentation-project-id
)
1055 (:= 'sys-presentation.measurement-id
1056 'sys-measurement.measurement-id
)
1057 (:= 'sys-measurement.acquisition-project-id
1058 'sys-acquisition-project.acquisition-project-id
)))
1063 "While fetching common-table-names of presentation-project-id ~D: ~A"
1064 presentation-project-id c
))))
1066 (defun encode-geojson-to-string (features &key junk-keys
)
1067 "Encode a list of property lists into a GeoJSON FeatureCollection.
1068 Each property list must contain keys for coordinates, :x, :y, :z; it
1069 may contain a numeric point :id and zero or more pieces of extra
1070 information. The extra information is stored as GeoJSON Feature
1071 properties. Exclude property list elements with keys that are in
1073 (with-output-to-string (s)
1074 (json:with-object
(s)
1075 (json:encode-object-member
:type
:*feature-collection s
)
1076 (json:as-object-member
(:features s
)
1077 (json:with-array
(s)
1079 #'(lambda (point-with-properties)
1080 (dolist (junk-key junk-keys
)
1081 (remf point-with-properties junk-key
))
1082 (destructuring-bind (&key x y z id
&allow-other-keys
) ;TODO: z probably bogus
1083 point-with-properties
1084 (json:as-array-member
(s)
1085 (json:with-object
(s)
1086 (json:encode-object-member
:type
:*feature s
)
1087 (json:as-object-member
(:geometry s
)
1088 (json:with-object
(s)
1089 (json:encode-object-member
:type
:*point s
)
1090 (json:as-object-member
(:coordinates s
)
1091 (json:encode-json
(list x y z
) s
))))
1092 (json:encode-object-member
:id id s
)
1093 (json:as-object-member
(:properties s
)
1094 (dolist (key '(:x
:y
:z
:id
))
1095 (remf point-with-properties key
))
1096 (json:encode-json-plist point-with-properties s
))))))
1098 (json:encode-object-member
:phoros-version
(phoros-version) s
))))
1101 "Return a WKT-compliant BOX3D string from string bbox."
1102 (concatenate 'string
"BOX3D("
1103 (substitute #\Space
#\
,
1104 (substitute #\Space
#\
, bbox
:count
1)
1105 :from-end t
:count
1)
1108 (hunchentoot:define-easy-handler
(points :uri
"/phoros/lib/points.json") (bbox)
1109 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
1110 (assert-authentication)
1111 (setf (hunchentoot:content-type
*) "application/json")
1113 (with-connection *postgresql-credentials
*
1114 (let* ((presentation-project-id
1115 (hunchentoot:session-value
'presentation-project-id
))
1117 (common-table-names presentation-project-id
)))
1118 (encode-geojson-to-string
1125 for common-table-name in common-table-names
1126 for aggregate-view-name
1127 = (point-data-table-name common-table-name
)
1128 ;; would have been nice, was too slow:
1129 ;; = (aggregate-view-name common-table-name)
1132 (:as
(:st_x
'coordinates
) x
)
1133 (:as
(:st_y
'coordinates
) y
)
1134 (:as
(:st_z
'coordinates
) z
)
1135 (:as
'point-id
'id
) ;becomes fid on client
1137 :distinct-on
'random
1138 :from
',aggregate-view-name
1139 :natural
:left-join
'sys-presentation
1142 (:= 'presentation-project-id
1143 ,presentation-project-id
)
1146 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
1147 ,*standard-coordinates
*))))))
1149 ,*number-of-features-per-layer
*))
1151 :junk-keys
'(:random
))))
1154 :error
"While fetching points from inside bbox ~S: ~A"
1157 (hunchentoot:define-easy-handler
1158 (aux-points :uri
"/phoros/lib/aux-points.json")
1160 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
1161 (assert-authentication)
1162 (setf (hunchentoot:content-type
*) "application/json")
1164 (let ((limit *number-of-features-per-layer
*)
1166 (aux-point-view-name (hunchentoot:session-value
1167 'presentation-project-name
))))
1168 (encode-geojson-to-string
1169 (with-connection *postgresql-aux-credentials
*
1175 (:as
(:st_x
'coordinates
) 'x
)
1176 (:as
(:st_y
'coordinates
) 'y
)
1177 (:as
(:st_z
'coordinates
) 'z
)
1178 :from
,aux-view-name
1181 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
1182 ,*standard-coordinates
*)))
1188 :error
"While fetching aux-points from inside bbox ~S: ~A"
1191 (hunchentoot:define-easy-handler
1192 (aux-local-data :uri
"/phoros/lib/aux-local-data"
1193 :default-request-type
:post
)
1195 "Receive coordinates, respond with the count nearest json objects
1196 containing arrays aux-numeric, aux-text, and distance to the
1197 coordinates received, wrapped in an array."
1198 (assert-authentication)
1199 (setf (hunchentoot:content-type
*) "application/json")
1200 (let* ((aux-view-name
1201 (aux-point-view-name (hunchentoot:session-value
1202 'presentation-project-name
)))
1203 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1204 (longitude (cdr (assoc :longitude data
)))
1205 (latitude (cdr (assoc :latitude data
)))
1206 (count (cdr (assoc :count data
)))
1208 (format nil
"POINT(~F ~F)" longitude latitude
))
1209 (snap-distance 1e-3) ;about 100 m, TODO: make this a defparameter
1211 (format nil
"~A,~A,~A,~A"
1212 (- longitude snap-distance
)
1213 (- latitude snap-distance
)
1214 (+ longitude snap-distance
)
1215 (+ latitude snap-distance
))))
1216 (encode-geojson-to-string
1218 (with-connection *postgresql-aux-credentials
*
1225 (:as
(:st_x
'coordinates
) 'x
)
1226 (:as
(:st_y
'coordinates
) 'y
)
1227 (:as
(:st_z
'coordinates
) 'z
)
1234 ,*spherical-mercator
*)
1236 (:st_geomfromtext
,point-form
,*standard-coordinates
*)
1237 ,*spherical-mercator
*))
1239 :from
',aux-view-name
1240 :where
(:&& 'coordinates
1242 ,(box3d bounding-box
) box3d
)
1243 ,*standard-coordinates
*)))
1248 (defun nillify-null (x)
1249 "Replace occurences of :null in nested sequence x by nil."
1250 (cond ((eq :null x
) nil
)
1254 (t (map (type-of x
) #'nillify-null x
))))
1256 (defun nullify-nil (x)
1257 "Replace occurences of nil in nested sequence x by :null."
1258 (cond ((null x
) :null
)
1262 (t (map (type-of x
) #'nullify-nil x
))))
1264 (hunchentoot:define-easy-handler
1265 (aux-local-linestring :uri
"/phoros/lib/aux-local-linestring.json"
1266 :default-request-type
:post
)
1268 "Receive longitude, latitude, radius, and step-size; respond
1269 with a JSON object comprising the elements linestring (a WKT
1270 linestring stitched together of the nearest auxiliary points from
1271 within radius around coordinates), current-point (the point on
1272 linestring closest to coordinates), and previous-point and next-point
1273 \(points on linestring step-size before and after current-point
1274 respectively). Wipe away any unfinished business first."
1275 (assert-authentication)
1276 (dolist (old-thread (hunchentoot:session-value
'recent-threads
))
1278 (bt:interrupt-thread old-thread
1279 #'(lambda () (signal 'superseded
)))))
1280 (setf (hunchentoot:session-value
'recent-threads
) nil
)
1281 (setf (hunchentoot:session-value
'number-of-threads
) 1)
1282 (push (bt:current-thread
) (hunchentoot:session-value
'recent-threads
))
1283 (setf (hunchentoot:content-type
*) "application/json")
1285 (let* ((thread-aux-points-function-name
1286 (thread-aux-points-function-name (hunchentoot:session-value
1287 'presentation-project-name
)))
1288 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1289 (longitude (cdr (assoc :longitude data
)))
1290 (latitude (cdr (assoc :latitude data
)))
1291 (radius (cdr (assoc :radius data
)))
1292 (step-size (cdr (assoc :step-size data
)))
1293 (azimuth (if (numberp (cdr (assoc :azimuth data
)))
1294 (cdr (assoc :azimuth data
))
1297 (format nil
"POINT(~F ~F)" longitude latitude
))
1300 (with-connection *postgresql-aux-credentials
*
1305 (,thread-aux-points-function-name
1307 ,point-form
,*standard-coordinates
*)
1309 ,*number-of-points-per-aux-linestring
*
1312 ,(proj:degrees-to-radians
91))))
1314 (with-output-to-string (s)
1315 (json:with-object
(s)
1316 (json:encode-object-member
1317 :linestring
(getf sql-response
:threaded-points
) s
)
1318 (json:encode-object-member
1319 :current-point
(getf sql-response
:current-point
) s
)
1320 (json:encode-object-member
1321 :previous-point
(getf sql-response
:back-point
) s
)
1322 (json:encode-object-member
1323 :next-point
(getf sql-response
:forward-point
) s
)
1324 (json:encode-object-member
1325 :azimuth
(getf sql-response
:new-azimuth
) s
))))
1327 ;; (setf (hunchentoot:return-code*) hunchentoot:+http-gateway-time-out+)
1330 (defun get-user-points (user-point-table-name &key
1331 (bounding-box "-180,-90,180,90")
1333 (order-criterion 'id
)
1335 "Return limit points from user-point-table-name in GeoJSON format,
1336 and the number of points returned."
1337 (let ((user-point-plist
1343 (:as
(:st_x
'coordinates
) 'x
)
1344 (:as
(:st_y
'coordinates
) 'y
)
1345 (:as
(:st_z
'coordinates
) 'z
)
1346 (:as
'user-point-id
'id
) ;becomes fid in OpenLayers
1347 ;; 'stdx-global 'stdy-global 'stdz-global
1349 'kind
'description
'numeric-description
1351 (:as
(:to-char
'creation-date
1352 ,*user-point-creation-date-format
*)
1354 'aux-numeric
'aux-text
1355 :from
,user-point-table-name
:natural
:left-join
'sys-user
1356 :where
(:&& 'coordinates
1357 (:st_setsrid
(:type
,(box3d bounding-box
) box3d
)
1358 ,*standard-coordinates
*)))
1365 (encode-geojson-to-string (nillify-null user-point-plist
)))
1366 (encode-geojson-to-string (nillify-null user-point-plist
)))
1367 (length user-point-plist
))))
1369 (hunchentoot:define-easy-handler
1370 (user-points :uri
"/phoros/lib/user-points.json")
1372 "Send *number-of-features-per-layer* randomly chosen GeoJSON-encoded
1373 points from inside bbox to client. If there is no bbox parameter,
1374 send all points and indent GeoJSON to make it more readable."
1375 (assert-authentication)
1376 (setf (hunchentoot:content-type
*) "application/json")
1378 (let ((bounding-box (or bbox
"-180,-90,180,90"))
1380 (limit (if bbox
*number-of-features-per-layer
* :null
))
1381 (order-criterion (if bbox
'(:random
) 'id
))
1382 (user-point-table-name
1383 (user-point-table-name (hunchentoot:session-value
1384 'presentation-project-name
))))
1385 (with-connection *postgresql-credentials
*
1386 (nth-value 0 (get-user-points user-point-table-name
1387 :bounding-box bounding-box
1389 :order-criterion order-criterion
1393 :error
"While fetching user-points~@[ from inside bbox ~S~]: ~A"
1396 (hunchentoot:define-easy-handler
1397 (user-point-attributes :uri
"/phoros/lib/user-point-attributes.json")
1399 "Send JSON object comprising arrays kinds and descriptions,
1400 each containing unique values called kind and description
1401 respectively, and count being the frequency of value in the user point
1403 (assert-authentication)
1404 (setf (hunchentoot:content-type
*) "application/json")
1406 (let ((user-point-table-name
1407 (user-point-table-name (hunchentoot:session-value
1408 'presentation-project-name
))))
1409 (with-connection *postgresql-credentials
*
1410 (with-output-to-string (s)
1411 (json:with-object
(s)
1412 (json:as-object-member
(:descriptions s
)
1413 (json:with-array
(s)
1414 (mapcar #'(lambda (x) (json:as-array-member
(s)
1415 (json:encode-json-plist x s
)))
1419 (:select
'description
1420 (:count
'description
)
1421 :from user-point-table-name
1422 :group-by
'description
)
1426 (json:as-object-member
(:kinds s
)
1427 (json:with-array
(s)
1428 (mapcar #'(lambda (x) (json:as-array-member
(s)
1429 (json:encode-json-plist x s
)))
1430 (query (format nil
"~
1431 (SELECT kind, count(kind) ~
1432 FROM ((SELECT kind FROM ~A) ~
1435 FROM (VALUES ('solitary'), ~
1438 AS defaults(kind))) ~
1439 AS kinds_union(kind) ~
1441 ORDER BY kind LIMIT 100"
1442 ;; Counts of solitary,
1443 ;; polyline, polygon may be
1444 ;; too big by one if we
1445 ;; collect them like this.
1446 (s-sql:to-sql-name user-point-table-name
))
1450 :error
"While fetching user-point-attributes: ~A"
1453 (hunchentoot:define-easy-handler photo-handler
1454 ((bayer-pattern :init-form
"65280,16711680")
1455 (color-raiser :init-form
"1,1,1")
1456 (mounting-angle :init-form
"0")
1458 "Serve an image from a .pictures file."
1459 (assert-authentication)
1463 (push (bt:current-thread
)
1464 (hunchentoot:session-value
'recent-threads
))
1465 (incf (hunchentoot:session-value
'number-of-threads
)))
1467 (cl-utilities:split-sequence
#\
/
1468 (hunchentoot:script-name
*)
1469 :remove-empty-subseqs t
))
1471 (cdddr ;remove leading phoros, lib, photo
1474 (cl-utilities:split-sequence
#\.
(first (last s
2))))
1476 (parse-integer (car (last s
)) :junk-allowed t
))
1481 :directory
(append (pathname-directory *common-root
*)
1484 :name
(first file-name-and-type
)
1485 :type
(second file-name-and-type
)))))
1487 (flex:with-output-to-sequence
(stream)
1489 stream path-to-file byte-position
1491 (apply #'vector
(mapcar
1493 (cl-utilities:split-sequence
1494 #\
, bayer-pattern
)))
1496 (apply #'vector
(mapcar
1497 #'parse-number
:parse-positive-real-number
1498 (cl-utilities:split-sequence
1501 :reversep
(= 180 (parse-integer mounting-angle
))
1502 :brightenp brightenp
))))
1503 (setf (hunchentoot:header-out
'cache-control
)
1504 (format nil
"max-age=~D" *browser-cache-max-age
*))
1505 (setf (hunchentoot:content-type
*) "image/png")
1507 (decf (hunchentoot:session-value
'number-of-threads
)))
1509 (setf (hunchentoot:return-code
*) hunchentoot
:+http-gateway-time-out
+)
1513 :error
"While serving image ~S: ~A" (hunchentoot:request-uri
*) c
))))
1515 (pushnew (hunchentoot:create-prefix-dispatcher
"/phoros/lib/photo"
1517 hunchentoot
:*dispatch-table
*)
1519 ;;; for debugging; this is the multi-file OpenLayers
1520 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1521 "/phoros/lib/openlayers/" "OpenLayers-2.10/")
1522 hunchentoot
:*dispatch-table
*)
1524 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1525 "/phoros/lib/ol/" "ol/")
1526 hunchentoot
:*dispatch-table
*)
1528 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1529 "/phoros/lib/public_html/" "public_html/")
1530 hunchentoot
:*dispatch-table
*)
1532 (pushnew (hunchentoot:create-static-file-dispatcher-and-handler
1533 "/favicon.ico" "public_html/favicon.ico")
1534 hunchentoot
:*dispatch-table
*)
1536 (hunchentoot:define-easy-handler
1537 (view :uri
(format nil
"/phoros/lib/view-~A" (phoros-version))
1538 :default-request-type
:post
)
1540 "Serve the client their main workspace."
1542 (hunchentoot:session-value
'authenticated-p
)
1543 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
1549 "Phoros: " (hunchentoot:session-value
1550 'presentation-project-name
))))
1551 (if (cli:verbosity-level
:use-multi-file-openlayers
)
1554 :src
(format nil
"/~A/lib/openlayers/lib/Firebug/firebug.js"
1557 :src
(format nil
"/~A/lib/openlayers/lib/OpenLayers.js"
1561 :src
(format nil
"/~A/lib/ol/OpenLayers.js"
1563 (:link
:rel
"stylesheet"
1564 :href
(format nil
"/~A/lib/css-~A/style.css"
1568 (:script
:src
(format ;variability in script name is
1569 nil
; supposed to fight browser cache
1570 "/~A/lib/phoros-~A-~A-~A.js"
1573 (hunchentoot:session-value
'user-name
)
1574 (hunchentoot:session-value
'presentation-project-name
)))
1575 (:script
:src
"http://maps.google.com/maps/api/js?sensor=false"))
1578 (:noscript
(:b
(:em
"You can't do much without JavaScript here.")))
1581 "Phoros: " (who:str
(hunchentoot:session-value
'user-full-name
))
1582 (who:fmt
" (~A)" (hunchentoot:session-value
'user-name
))
1583 "with " (:span
:id
"user-role"
1584 (who:str
(hunchentoot:session-value
'user-role
)))
1586 (:span
:id
"presentation-project-name"
1587 (who:str
(hunchentoot:session-value
1588 'presentation-project-name
)))
1589 (:span
:id
"presentation-project-emptiness")
1590 (:span
:id
"recommend-fresh-login")
1591 (:span
:class
"h1-right"
1592 (:span
:id
"caching-indicator")
1593 (:span
:id
"phoros-version"
1594 (who:fmt
"v~A" (phoros-version)))))
1595 ;; streetmap area (northwest)
1597 :class
"controlled-streetmap"
1598 (:div
:id
"streetmap" :class
"streetmap" :style
"cursor:crosshair")
1599 (:div
:id
"streetmap-controls" :class
"streetmap-controls"
1600 (:div
:id
"streetmap-vertical-strut"
1601 :class
"streetmap-vertical-strut")
1602 (:div
:id
"streetmap-layer-switcher"
1603 :class
"streetmap-layer-switcher")
1604 (:button
:id
"unselect-all-restrictions-button"
1606 :onclick
(ps-inline (unselect-all-restrictions))
1608 (:select
:id
"restriction-select"
1609 :name
"restriction-select"
1612 :onchange
(ps-inline (request-photos)))
1613 (:div
:id
"streetmap-overview" :class
"streetmap-overview")
1614 (:div
:id
"streetmap-mouse-position"
1615 :class
"streetmap-mouse-position")
1616 (:div
:id
"streetmap-zoom" :class
"streetmap-zoom")))
1617 ;; control area (north)
1619 :class
"phoros-controls" :id
"phoros-controls"
1620 (:div
:id
"real-phoros-controls"
1621 (:h2
:class
"point-creator h2-phoros-controls"
1623 (:h2
:class
"point-editor h2-phoros-controls"
1625 (:span
:id
"creator"))
1626 (:h2
:class
"point-viewer h2-phoros-controls"
1628 (:span
:id
"creator"))
1629 (:h2
:class
"aux-data-viewer h2-phoros-controls"
1630 "View Auxiliary Data")
1631 (:h2
:class
"multiple-points-viewer"
1632 "Multiple Points Selected")
1633 (:div
:class
"multiple-points-viewer"
1634 (:p
"You have selected multiple user points.")
1635 (:p
"Unselect all but one to edit or view its properties."))
1636 (:span
:class
"point-creator point-editor point-viewer"
1641 :id
"point-kind-select"
1642 :name
"point-kind-select"
1643 :class
"combobox-select write-permission-dependent"
1644 :onchange
(ps-inline
1645 (consolidate-combobox
1649 :id
"point-kind-input"
1650 :name
"point-kind-input"
1651 :class
"combobox-input write-permission-dependent"
1652 :onchange
(ps-inline
1653 (unselect-combobox-selection
1657 (:input
:id
"point-numeric-description"
1658 :class
"vanilla-input write-permission-dependent"
1660 :type
"text" :name
"point-numeric-description")
1663 :id
"point-description"
1666 :id
"point-description-select"
1667 :name
"point-description-select"
1668 :class
"combobox-select write-permission-dependent"
1669 :onchange
(ps-inline
1670 (consolidate-combobox
1671 "point-description"))
1674 :id
"point-description-input"
1675 :name
"point-description-input"
1676 :class
"combobox-input write-permission-dependent"
1677 :onchange
(ps-inline
1678 (unselect-combobox-selection
1679 "point-description"))
1682 (:button
:id
"delete-point-button" :disabled t
1684 :onclick
(ps-inline (delete-point))
1686 (:button
:disabled t
:id
"finish-point-button"
1689 (:div
:id
"uniquify-buttons"
1690 (:button
:id
"suggest-unique-button"
1693 (insert-unique-suggestion))
1695 (:button
:id
"force-duplicate-button"
1698 (:div
:id
"aux-point-distance-or-point-creation-date"
1699 (:code
:id
"point-creation-date"
1700 :class
"point-editor point-viewer")
1702 :id
"aux-point-distance" :disabled t
1703 :class
"point-creator aux-data-viewer aux-data-dependent"
1704 :size
1 :name
"aux-point-distance"
1705 :onchange
(ps-inline
1706 (aux-point-distance-selected))
1708 (enable-aux-point-selection)))
1710 :id
"include-aux-data"
1711 :class
"point-creator aux-data-dependent"
1713 (:input
:id
"include-aux-data-p"
1714 :class
"tight-input"
1715 :type
"checkbox" :checked t
1716 :name
"include-aux-data-p"
1717 :onchange
(ps-inline
1718 (flip-aux-data-inclusion)))
1720 (:div
:id
"display-nearest-aux-data"
1721 :class
"aux-data-viewer"
1723 (:input
:id
"display-nearest-aux-data-p"
1724 :class
"tight-input"
1725 :type
"checkbox" :checked t
1726 :name
"display-nearest-aux-data-p"
1727 :onchange
(ps-inline
1728 (flip-nearest-aux-data-display)))
1732 :class
"point-creator point-editor point-viewer aux-data-viewer"
1733 (:div
:id
"aux-numeric-list")
1734 (:div
:id
"aux-text-list")))
1735 (:div
:class
"walk-mode-controls"
1736 (:div
:id
"walk-mode"
1737 :class
"aux-data-dependent"
1738 (:input
:id
"walk-p"
1739 :class
"tight-input"
1740 :type
"checkbox" :checked nil
1741 :onchange
(ps-inline
1743 (:label
:for
"walk-p"
1745 (:div
:id
"decrease-step-size"
1746 :class
"aux-data-dependent"
1747 :onclick
(ps-inline (decrease-step-size)))
1748 (:div
:id
"step-size"
1749 :class
"aux-data-dependent"
1750 :onclick
(ps-inline (increase-step-size))
1752 (:div
:id
"increase-step-size"
1753 :class
"aux-data-dependent"
1754 :onclick
(ps-inline (increase-step-size))
1755 :ondblclick
(ps-inline (increase-step-size)
1756 (increase-step-size)))
1757 (:div
:id
"step-button" :disabled nil
1758 :class
"aux-data-dependent"
1759 :onclick
(ps-inline (step))
1760 :ondblclick
(ps-inline (step t
))
1762 (:div
:class
"image-main-controls"
1763 (:div
:id
"auto-zoom"
1764 (:input
:id
"zoom-to-point-p"
1765 :class
"tight-input"
1766 :type
"checkbox" :checked t
)
1767 (:label
:for
"zoom-to-point-p"
1769 (:div
:id
"brighten-images"
1770 (:input
:id
"brighten-images-p"
1771 :class
"tight-input"
1772 :type
"checkbox" :checked nil
)
1773 (:label
:for
"brighten-images-p"
1775 (:div
:id
"zoom-images-to-max-extent"
1776 :onclick
(ps-inline (zoom-images-to-max-extent)))
1777 (:div
:id
"no-footprints-p"
1779 (:div
:id
"remove-work-layers-button" :disabled t
1780 :onclick
(ps-inline (reset-layers-and-controls))
1782 ;; help area (northeast)
1786 :id
"download-user-points-button"
1788 :onclick
(format nil
1789 "self.location.href = \"/~A/lib/user-points.json\""
1791 "download points") ;TODO: offer other formats and maybe projections
1800 "/lib/blurb?openlayers-version="
1801 (@ *open-layers
*version_number
*))
1803 (:img
:src
(format nil
"/~A/lib/public_html/phoros-logo-plain.png"
1805 :alt
"Phoros" :style
"vertical-align:middle"
1807 (:button
:id
"logout-button"
1809 :onclick
(ps-inline (bye))
1811 (:h2
:id
"h2-help" "Help")
1812 (:div
:id
"help-display"))
1813 ;; image area (south)
1814 (:div
:id
"images" :style
"clear:both"
1816 for i from
0 below
*number-of-images
* do
1818 (:div
:class
"controlled-image"
1819 (:div
:id
(format nil
"image-~S-controls" i
)
1820 :class
"image-controls"
1821 (:div
:id
(format nil
"image-~S-zoom" i
)
1822 :class
"image-zoom")
1823 (:div
:id
(format nil
"image-~S-layer-switcher" i
)
1824 :class
"image-layer-switcher")
1825 (:div
:id
(format nil
"image-~S-usable" i
)
1826 :class
"image-usable"
1828 (:div
:id
(format nil
"image-~S-trigger-time" i
)
1829 :class
"image-trigger-time"))
1830 (:div
:id
(format nil
"image-~S" i
)
1831 :class
"image" :style
"cursor:crosshair"))))))))
1832 (hunchentoot:redirect
1833 (format nil
"/~A/~A"
1835 (hunchentoot:session-value
'presentation-project-name
))
1836 :add-session-id t
)))
1838 (hunchentoot:define-easy-handler
1839 (epipolar-line :uri
"/phoros/lib/epipolar-line")
1841 "Receive vector of two sets of picture parameters, the first of
1842 which containing coordinates (m, n) of a clicked point. Respond with a
1843 JSON encoded epipolar-line."
1844 (assert-authentication)
1845 (setf (hunchentoot:content-type
*) "application/json")
1846 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
1847 (json:encode-json-to-string
1848 (photogrammetry :epipolar-line
(first data
) (second data
)))))
1850 (hunchentoot:define-easy-handler
1851 (estimated-positions :uri
"/phoros/lib/estimated-positions")
1853 "Receive a two-part JSON vector comprising (1) a vector containing
1854 sets of picture-parameters with clicked (\"active\") points
1855 stored in :m, :n; and (2) a vector containing sets of
1856 picture-parameters; respond with a JSON encoded two-part vector
1857 comprising (1) a point in global coordinates; and (2) a vector of
1858 image coordinates (m, n) for the global point that correspond to the
1859 images from the received second vector. TODO: report error on bad
1860 data (ex: points too far apart)."
1861 ;; TODO: global-point-for-display should probably contain a proj string in order to make sense of the (cartesian) standard deviations.
1862 (assert-authentication)
1863 (setf (hunchentoot:content-type
*) "application/json")
1865 (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1866 (active-point-photo-parameters
1868 (number-of-active-points
1869 (length active-point-photo-parameters
))
1870 (destination-photo-parameters
1873 (cdr (assoc :cartesian-system
1874 (first active-point-photo-parameters
))))
1875 (global-point-cartesian
1877 :multi-position-intersection active-point-photo-parameters
))
1878 (global-point-geographic-radians
1879 (proj:cs2cs
(list (cdr (assoc :x-global global-point-cartesian
))
1880 (cdr (assoc :y-global global-point-cartesian
))
1881 (cdr (assoc :z-global global-point-cartesian
)))
1882 :source-cs cartesian-system
))
1883 (global-point-for-display ;points geographic cs, degrees; std deviations in cartesian cs
1884 (pairlis '(:longitude
:latitude
:ellipsoid-height
1885 ;; :stdx-global :stdy-global :stdz-global
1888 (proj:radians-to-degrees
1889 (first global-point-geographic-radians
))
1890 (proj:radians-to-degrees
1891 (second global-point-geographic-radians
))
1892 (third global-point-geographic-radians
)
1893 ;; (cdr (assoc :stdx-global global-point-cartesian))
1894 ;; (cdr (assoc :stdy-global global-point-cartesian))
1895 ;; (cdr (assoc :stdz-global global-point-cartesian))
1896 number-of-active-points
)))
1899 for i in destination-photo-parameters
1902 (photogrammetry :reprojection i global-point-cartesian
)))))
1903 (json:encode-json-to-string
1904 (list global-point-for-display image-coordinates
))))
1906 (hunchentoot:define-easy-handler
1907 (user-point-positions :uri
"/phoros/lib/user-point-positions")
1909 "Receive a two-part JSON vector comprising
1910 - a vector of user-point-id's and
1911 - a vector containing sets of picture-parameters;
1912 respond with a JSON object comprising the elements
1913 - image-points, a vector whose elements
1914 - correspond to the elements of the picture-parameters vector
1916 - are GeoJSON feature collections containing one point (in picture
1917 coordinates) for each user-point-id received;
1918 - user-point-count, the number of user-points we tried to fetch
1920 (assert-authentication)
1921 (setf (hunchentoot:content-type
*) "application/json")
1922 (with-connection *postgresql-credentials
*
1923 (let* ((user-point-table-name
1924 (user-point-table-name (hunchentoot:session-value
1925 'presentation-project-name
)))
1926 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1927 (user-point-ids (first data
))
1928 (user-point-count (length user-point-ids
))
1929 (destination-photo-parameters (second data
))
1931 (cdr (assoc :cartesian-system
1932 (first destination-photo-parameters
)))) ;TODO: in rare cases, coordinate systems of the images shown may differ
1936 (:as
(:st_x
'coordinates
) 'longitude
)
1937 (:as
(:st_y
'coordinates
) 'latitude
)
1938 (:as
(:st_z
'coordinates
) 'ellipsoid-height
)
1939 (:as
'user-point-id
'id
) ;becomes fid on client
1942 'numeric-description
1944 (:as
(:to-char
'creation-date
"IYYY-MM-DD HH24:MI:SS TZ")
1948 :from user-point-table-name
:natural
:left-join
'sys-user
1949 :where
(:in
'user-point-id
(:set user-point-ids
)))
1951 (global-points-cartesian
1953 for global-point-geographic in user-points
1955 (ignore-errors ;in case no destination-photo-parameters have been sent
1956 (pairlis '(:x-global
:y-global
:z-global
)
1959 (proj:degrees-to-radians
1960 (getf global-point-geographic
:longitude
))
1961 (proj:degrees-to-radians
1962 (getf global-point-geographic
:latitude
))
1963 (getf global-point-geographic
:ellipsoid-height
))
1964 :destination-cs cartesian-system
)))))
1967 for photo-parameter-set in destination-photo-parameters
1969 (encode-geojson-to-string
1971 for global-point-cartesian in global-points-cartesian
1972 for user-point in user-points
1974 (when (point-within-image-p
1975 (getf user-point
:id
)
1976 (hunchentoot:session-value
'presentation-project-name
)
1977 (cdr (assoc :byte-position photo-parameter-set
))
1978 (cdr (assoc :filename photo-parameter-set
))
1979 (cdr (assoc :measurement-id photo-parameter-set
)))
1981 (let ((photo-coordinates
1982 (photogrammetry :reprojection
1984 global-point-cartesian
))
1987 (setf (getf photo-point
:x
)
1988 (cdr (assoc :m photo-coordinates
)))
1989 (setf (getf photo-point
:y
)
1990 (cdr (assoc :n photo-coordinates
)))
1992 :junk-keys
'(:longitude
:latitude
:ellipsoid-height
)))))
1993 (with-output-to-string (s)
1994 (json:with-object
(s)
1995 (json:encode-object-member
:user-point-count user-point-count s
)
1996 (json:as-object-member
(:image-points s
)
1997 (json:with-array
(s)
1998 (loop for i in image-coordinates do
1999 (json:as-array-member
(s) (princ i s
))))))))))
2001 (defun point-within-image-p (user-point-id presentation-project-name
2002 byte-position filename measurement-id
)
2003 "Return t if either point with user-point-id is inside the footprint
2004 of the image described by byte-position, filename, and measurement-id;
2005 or if that image doesn't have a footprint. Return nil otherwise."
2006 (let* ((user-point-table-name (user-point-table-name
2007 presentation-project-name
))
2008 (presentation-project-id (presentation-project-id-from-name
2009 presentation-project-name
))
2010 (common-table-names (common-table-names presentation-project-id
)))
2015 for common-table-name in common-table-names
2016 for aggregate-view-name
2017 = (aggregate-view-name common-table-name
)
2021 :from
',aggregate-view-name
2022 :where
(:and
(:= 'byte-position
,byte-position
)
2023 (:= 'filename
,filename
)
2024 (:= 'measurement-id
,measurement-id
)
2025 (:or
(:is-null
'footprint
)
2027 (:select
'coordinates
2028 :from
,user-point-table-name
2029 :where
(:= 'user-point-id
2034 (hunchentoot:define-easy-handler
2035 (multi-position-intersection :uri
"/phoros/lib/intersection")
2037 "Receive vector of sets of picture parameters, respond with stuff."
2038 (assert-authentication)
2039 (setf (hunchentoot:content-type
*) "application/json")
2040 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
2041 (json:encode-json-to-string
2042 (photogrammetry :multi-position-intersection data
))))