1 ;;; PHOROS -- Photogrammetric Road Survey
2 ;;; Copyright (C) 2010, 2011, 2012, 2016 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 (assert (= 42 (imread:ping
42))) ;check imread
134 (initialize-leap-seconds) ;check source of leap second info
135 (format *error-output
* "~&OK~%"))
136 (error (e) (format *error-output
* "~A~&" e
))))
138 (defun muffle-postgresql-warnings ()
139 "For current DB, silence PostgreSQL's warnings about implicitly
141 (unless (cli:verbosity-level
:postgresql-warnings
)
142 (execute "SET client_min_messages TO ERROR;")))
144 (defun check-db (db-credentials)
145 "Check postgresql connection. Return t if successful; show error on
146 *error-output* otherwise. db-credentials is a list like so: (database
147 user password host &key (port 5432) use-ssl)."
150 (setf connection
(apply #'connect db-credentials
))
151 (error (e) (format *error-output
* "Database connection ~S failed: ~A~&"
154 (disconnect connection
)
157 (defun ignore-warnings (c) (declare (ignore c
)) (muffle-warning))
159 (defmethod hunchentoot:session-cookie-name
(acceptor)
160 (declare (ignore acceptor
))
163 (defun start-server (&key
(proxy-root "phoros") (http-port 8080) address
165 "Start the presentation project server which listens on http-port
166 at address. Address defaults to all addresses of the local machine."
167 (setf *phoros-server
*
168 (make-instance 'hunchentoot
:easy-acceptor
171 :document-root
(ensure-directories-exist
173 :error-template-directory
(ensure-directories-exist
174 "unexpected_html/errors/")))
175 (setf hunchentoot
:*session-max-time
* (* 3600 24))
176 (setf *proxy-root
* proxy-root
)
177 (setf *common-root
* common-root
)
178 (check-db *postgresql-credentials
*)
179 (with-restarting-connection *postgresql-credentials
*
180 (assert-phoros-db-major-version))
181 (hunchentoot:reset-session-secret
)
182 (hunchentoot:start
*phoros-server
*))
184 (defun stop-server () (hunchentoot:stop
*phoros-server
*))
186 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
187 (register-sql-operators :2+-ary
:&& :overlaps
))
189 (hunchentoot:define-easy-handler phoros-handler
()
190 "First HTTP contact: if necessary, check credentials, establish new
192 (with-restarting-connection *postgresql-credentials
*
193 (let* ((s (cl-utilities:split-sequence
195 (hunchentoot:script-name
*)
196 :remove-empty-subseqs t
))
197 (presentation-project-name (second s
))
198 (presentation-project-id
200 (presentation-project-id-from-name presentation-project-name
))))
202 ;; TODO: remove the following line (which seems to function as a
203 ;; wakeup call of sorts)...
204 (get-dao 'sys-user-role
0 0)
205 ;; ...and make sure the following error doesn't occur any longer
206 ;; while accessing the HTTP server:
207 ;; #<POSTMODERN:DAO-CLASS PHOROS::SYS-USER-ROLE> cannot be printed readably.
210 ((null presentation-project-id
)
211 (setf (hunchentoot:return-code
*) hunchentoot
:+http-not-found
+))
212 ((and (equal (hunchentoot:session-value
'presentation-project-name
)
213 presentation-project-name
)
214 (hunchentoot:session-value
'authenticated-p
))
215 (hunchentoot:redirect
216 (format nil
"/~A/lib/view-~A"
223 (setf (hunchentoot:session-value
'presentation-project-name
)
224 presentation-project-name
)
225 (setf (hunchentoot:session-value
'presentation-project-id
)
226 presentation-project-id
)
227 (setf (hunchentoot:session-value
'presentation-project-bbox
)
230 (bounding-box (get-dao 'sys-presentation-project
231 presentation-project-name
)))))
232 (if (or (null bbox
) (eq :null bbox
))
235 (setf (hunchentoot:session-value
'aux-data-p
)
236 (with-restarting-connection *postgresql-aux-credentials
*
237 (view-exists-p (aux-point-view-name
238 presentation-project-name
))))
239 (setf (hunchentoot:session-value
'number-of-threads
) 0)
240 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
242 :style
"font-family:sans-serif;"
244 :method
"post" :enctype
"multipart/form-data"
245 :action
(format nil
"/~A/lib/authenticate"
249 (:legend
(:b
(:a
:href
"http://phoros.boundp.org"
250 :style
"text-decoration:none;"
252 (who:fmt
" [~A]" presentation-project-name
)))
254 (:b
(:em
"You can't do much without JavaScript there.")))
257 (:input
:type
"text" :name
"user-name"))
260 (:input
:type
"password" :name
"user-password")
262 (:span
:id
"cackle"))
263 (:input
:type
"submit" :value
"Submit"
265 (setf (chain document
266 (get-element-by-id "cackle")
268 "Ok, let's see…"))))
269 (:script
:type
"text/javascript"
270 (who:str
(ps (chain document
275 for i in
*login-intro
*
276 do
(who:htm
(:p
(who:str i
))))))))))))
278 (pushnew (hunchentoot:create-regex-dispatcher
"/phoros/(?!lib/)"
280 hunchentoot
:*dispatch-table
*)
282 (defun stored-bbox ()
283 "Return stored bounding box for user and presentation project of
285 (with-restarting-connection *postgresql-credentials
*
286 (let ((bbox (bounding-box
287 (get-dao 'sys-user-role
288 (hunchentoot:session-value
290 (hunchentoot:session-value
291 'presentation-project-id
)))))
293 (hunchentoot:session-value
'presentation-project-bbox
)
296 (defun stored-cursor ()
297 "Return stored cursor position for user and presentation project of
299 (with-restarting-connection *postgresql-credentials
*
302 (:select
(:st_x
'cursor
) (:st_y
'cursor
)
304 :where
(:and
(:= 'user-id
305 (hunchentoot:session-value
'user-id
))
306 (:= 'presentation-project-id
307 (hunchentoot:session-value
308 'presentation-project-id
))
309 (:raw
"cursor IS NOT NULL")))
312 (format nil
"~{~F~#^,~}" cursor
)))))
315 (hunchentoot:define-easy-handler
316 (authenticate-handler :uri
"/phoros/lib/authenticate"
317 :default-request-type
:post
)
319 "Check user credentials."
320 (with-restarting-connection *postgresql-credentials
*
321 (let* ((user-name (hunchentoot:post-parameter
"user-name"))
322 (user-password (hunchentoot:post-parameter
"user-password"))
323 (presentation-project-id (hunchentoot:session-value
324 'presentation-project-id
))
326 (when presentation-project-id
329 'sys-user.user-full-name
331 'sys-user-role.user-role
332 :from
'sys-user-role
'sys-user
334 (:= 'presentation-project-id presentation-project-id
)
335 (:= 'sys-user-role.user-id
'sys-user.user-id
)
336 (:= 'user-name user-name
)
337 (:= 'user-password user-password
)))
339 (user-full-name (first user-info
))
340 (user-id (second user-info
))
341 (user-role (third user-info
)))
344 (setf (hunchentoot:session-value
'authenticated-p
) t
345 (hunchentoot:session-value
'user-name
) user-name
346 (hunchentoot:session-value
'user-full-name
) user-full-name
347 (hunchentoot:session-value
'user-id
) user-id
348 (hunchentoot:session-value
'user-role
) user-role
)
349 (hunchentoot:redirect
350 (format nil
"/~A/lib/view-~A"
355 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
357 :style
"font-family:sans-serif;"
359 (:a
:href
(format nil
"/~A/~A/"
361 (hunchentoot:session-value
362 'presentation-project-name
))
365 (defun assert-authentication ()
366 "Abort request handler on unauthorized access."
367 (unless (hunchentoot:session-value
'authenticated-p
)
368 (setf (hunchentoot:return-code
*) hunchentoot
:+http-precondition-failed
+)
369 (hunchentoot:abort-request-handler
)))
371 (hunchentoot:define-easy-handler logout-handler
(bbox longitude latitude
)
372 (if (hunchentoot:session-value
'authenticated-p
)
373 (with-restarting-connection *postgresql-credentials
*
374 (let ((presentation-project-name
375 (hunchentoot:session-value
'presentation-project-name
))
377 (get-dao 'sys-user-role
378 (hunchentoot:session-value
'user-id
)
379 (hunchentoot:session-value
'presentation-project-id
))))
382 (setf (bounding-box sys-user-role
) bbox
))
383 (when (and longitude latitude
)
384 (let* ;; kludge: should be done by some library, not by DB query
385 ((point-form (format nil
"POINT(~F ~F)" longitude latitude
))
386 (point-wkb (query (:select
387 (:st_geomfromtext point-form
))
389 (setf (cursor sys-user-role
) point-wkb
)))
390 (update-dao sys-user-role
))
391 (hunchentoot:remove-session hunchentoot
:*session
*)
392 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
398 "Phoros: logged out" )))
399 (:link
:rel
"stylesheet"
400 :href
(format nil
"/~A/lib/css-~A/style.css"
405 (:h1
:id
"title" "Phoros: logged out")
406 (:p
"Log back in to project "
407 (:a
:href
(format nil
"/~A/~A"
409 presentation-project-name
)
410 (who:fmt
"~A." presentation-project-name
))))))))
413 (pushnew (hunchentoot:create-regex-dispatcher
"/logout" 'logout-handler
)
414 hunchentoot
:*dispatch-table
*)
416 (hunchentoot:define-easy-handler set-cursor-handler
(bbox longitude latitude
)
417 (assert-authentication)
418 (with-restarting-connection *postgresql-credentials
*
419 (let ((presentation-project-name
420 (hunchentoot:session-value
'presentation-project-name
))
422 (get-dao 'sys-user-role
423 (hunchentoot:session-value
'user-id
)
424 (hunchentoot:session-value
'presentation-project-id
))))
427 (setf (bounding-box sys-user-role
) bbox
))
428 (when (and longitude latitude
)
429 (let* ;; kludge: should be done by some library, not by DB query
430 ((point-form (format nil
"POINT(~F ~F)" longitude latitude
))
431 (point-wkb (query (:select
432 (:st_geomfromtext point-form
))
434 (setf (cursor sys-user-role
) point-wkb
)))
435 (update-dao sys-user-role
))))
438 (pushnew (hunchentoot:create-regex-dispatcher
"/set-cursor" 'set-cursor-handler
)
439 hunchentoot
:*dispatch-table
*)
441 (define-condition superseded
() ()
443 "Tell a thread to finish as soon as possible taking any shortcuts
446 (hunchentoot:define-easy-handler
447 (selectable-restrictions :uri
"/phoros/lib/selectable-restrictions.json"
448 :default-request-type
:post
)
450 "Respond with a list of restrictions the user may choose from."
451 (assert-authentication)
452 (setf (hunchentoot:content-type
*) "application/json")
453 (with-restarting-connection *postgresql-credentials
*
454 (json:encode-json-to-string
457 (:select
'restriction-id
458 :from
'sys-selectable-restriction
459 :where
(:= 'presentation-project-id
460 (hunchentoot:session-value
461 'presentation-project-id
)))
465 (defun selected-restrictions (presentation-project-id selected-restriction-ids
)
466 "Get from current database connection a list of restriction clauses
467 belonging to presentation-project-id and ids from list
468 selected-restriction-ids."
471 `(:select
'sql-clause
472 :from
'sys-selectable-restriction
473 :where
(:and
(:= 'presentation-project-id
474 ,presentation-project-id
)
476 ,@(loop for i in selected-restriction-ids
477 collect
(list := 'restriction-id i
))))))
480 (defun sql-where-conjunction (sql-boolean-clauses)
481 "Parenthesize sql-boolean-clauses and concatenate them into a
482 string, separated by \"AND\". Return \" TRUE \" if
483 sql-boolean-clauses is nil."
484 (if sql-boolean-clauses
485 (apply #'concatenate
'string
(butlast (loop
486 for i in sql-boolean-clauses
493 (hunchentoot:define-easy-handler
494 (nearest-image-data :uri
"/phoros/lib/nearest-image-data"
495 :default-request-type
:post
)
497 "Receive coordinates, respond with the count nearest json objects
498 containing picture url, calibration parameters, and car position,
499 wrapped in an array. Wipe away any unfinished business first."
500 (assert-authentication)
501 (dolist (old-thread (hunchentoot:session-value
'recent-threads
))
503 (bt:interrupt-thread old-thread
504 #'(lambda () (signal 'superseded
)))))
505 (setf (hunchentoot:session-value
'recent-threads
) nil
)
506 (setf (hunchentoot:session-value
'number-of-threads
) 1)
507 (push (bt:current-thread
) (hunchentoot:session-value
'recent-threads
))
508 (setf (hunchentoot:content-type
*) "application/json")
509 (with-restarting-connection *postgresql-credentials
*
510 (let* ((presentation-project-id (hunchentoot:session-value
511 'presentation-project-id
))
512 (common-table-names (common-table-names
513 presentation-project-id
))
514 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
515 (longitude (cdr (assoc :longitude data
)))
516 (latitude (cdr (assoc :latitude data
)))
517 (count (cdr (assoc :count data
)))
518 (zoom (cdr (assoc :zoom data
)))
519 (snap-distance ;bogus distance in degrees,
520 (* 100e-5 ; assuming geographic
521 (expt 2 (- ; coordinates
522 14 ; (1m = 1e-5 degrees)
525 (point-form (format nil
"POINT(~F ~F)" longitude latitude
))
526 (selected-restrictions-conjunction
527 (sql-where-conjunction
528 (selected-restrictions presentation-project-id
529 (cdr (assoc :selected-restriction-ids
531 (nearest-footprint-centroid-query
532 ;; Inserting the following into
533 ;; image-data-with-footprints-query as a subquery would
534 ;; work correctly but is way too slow.
539 ,@*aggregate-view-columns
*
545 for common-table-name
546 in common-table-names
547 for aggregate-view-name
548 = (aggregate-view-name
554 (:st_centroid
'footprint
)
557 ,*standard-coordinates
*))
559 (:as
(:st_centroid
'footprint
)
561 ,@*aggregate-view-columns
*
565 ;; no-ops wrt self-references in
566 ;; selected-restrictions-conjunction
567 ,@(postmodern-as-clauses
568 (pairlis *aggregate-view-columns
*
569 *aggregate-view-columns
*))
570 :from
',aggregate-view-name
)
571 'images-of-acquisition-project
)
574 (:= 'presentation-project-id
575 ,presentation-project-id
)
580 ,*standard-coordinates
*)
582 (:raw
,selected-restrictions-conjunction
)))))
586 (nearest-footprint-image
587 (ignore-errors (logged-query "centroid of nearest footprint"
588 nearest-footprint-centroid-query
590 (nearest-footprint-centroid
591 (cdr (assoc :centroid nearest-footprint-image
)))
592 (image-data-with-footprints-query
598 for common-table-name in common-table-names
599 for aggregate-view-name
600 = (aggregate-view-name common-table-name
)
603 ,@*aggregate-view-columns
*
606 ((:is-null
'footprint
) 'coordinates
)
607 (t (:st_centroid
'footprint
)))
608 ,nearest-footprint-centroid
)
610 (:as
(:not
(:is-null
'footprint
))
612 ,(when (cli:verbosity-level
:render-footprints
)
613 '(:as
(:st_asewkt
'footprint
)
618 ,@(postmodern-as-clauses
619 nearest-footprint-image
)
620 :from
',aggregate-view-name
)
621 'images-of-acquisition-project-plus-reference-image
)
624 (:= 'presentation-project-id
625 ,presentation-project-id
)
626 (:st_contains
'footprint
627 ,nearest-footprint-centroid
)
628 (:raw
,selected-restrictions-conjunction
)))))
631 (nearest-image-without-footprints-query
637 for common-table-name in common-table-names
638 for aggregate-view-name
639 = (aggregate-view-name common-table-name
)
642 ,@*aggregate-view-columns
*
643 (:as
(:st_distance
'coordinates
646 ,*standard-coordinates
*))
648 (:as
(:not
(:is-null
'footprint
))
653 ;; no-ops wrt self-references in
654 ;; selected-restrictions-conjunction
655 ,@(postmodern-as-clauses
656 (pairlis *aggregate-view-columns
*
657 *aggregate-view-columns
*))
658 :from
',aggregate-view-name
)
659 'images-of-acquisition-project
)
661 (:and
(:= 'presentation-project-id
662 ,presentation-project-id
)
663 (:st_dwithin
'coordinates
666 ,*standard-coordinates
*)
668 (:raw
,selected-restrictions-conjunction
)))))
671 (nearest-image-without-footprint
672 (unless nearest-footprint-centroid
;otherwise save time
673 (ignore-errors (logged-query "no footprint, first image"
674 nearest-image-without-footprints-query
676 (image-data-without-footprints-query
682 for common-table-name in common-table-names
683 for aggregate-view-name
684 = (aggregate-view-name common-table-name
)
687 ,@*aggregate-view-columns
*
688 (:as
(:st_distance
'coordinates
691 ,*standard-coordinates
*))
693 (:as
(:not
(:is-null
'footprint
))
698 ,@(postmodern-as-clauses
699 nearest-image-without-footprint
)
700 :from
',aggregate-view-name
)
701 'images-of-acquisition-project
)
703 (:and
(:= 'presentation-project-id
704 ,presentation-project-id
)
705 (:st_dwithin
'coordinates
708 ,*standard-coordinates
*)
710 (:raw
,selected-restrictions-conjunction
)))))
716 (if nearest-footprint-centroid
717 (logged-query "footprints are ready"
718 image-data-with-footprints-query
720 (logged-query "no footprints yet"
721 image-data-without-footprints-query
723 (superseded () nil
))))
724 (when (cli:verbosity-level
:render-footprints
)
728 for photo-parameter-set in result
729 for footprint-vertices
= ;something like this:
730 ;; "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))"
731 (ignore-errors ;probably no :footprint-wkt
734 (parse-number:parse-real-number x
))
735 (cl-utilities:split-sequence
#\Space p
)))
737 (cl-utilities:split-sequence-if
742 (cdr (assoc :footprint-wkt photo-parameter-set
)))
745 (if footprint-vertices
749 '(:type
:coordinates
)
753 for footprint-vertex in footprint-vertices
754 for reprojected-vertex
=
757 ;; KLUDGE: translate keys, e.g. a1 -> a_1
758 (json:decode-json-from-string
759 (json:encode-json-to-string photo-parameter-set
))
760 (pairlis '(:x-global
:y-global
:z-global
)
762 (list (proj:degrees-to-radians
763 (first footprint-vertex
))
764 (proj:degrees-to-radians
765 (second footprint-vertex
))
766 (third footprint-vertex
))
768 (cdr (assoc :cartesian-system
769 photo-parameter-set
)))))
771 (list (cdr (assoc :m reprojected-vertex
))
772 (cdr (assoc :n reprojected-vertex
))))))
774 photo-parameter-set
))))
775 (decf (hunchentoot:session-value
'number-of-threads
))
776 (json:encode-json-to-string result
))))
778 (hunchentoot:define-easy-handler
779 (store-point :uri
"/phoros/lib/store-point" :default-request-type
:post
)
781 "Receive point sent by user; store it into database."
782 (assert-authentication)
783 (let* ((presentation-project-name (hunchentoot:session-value
784 'presentation-project-name
))
785 (user-id (hunchentoot:session-value
'user-id
))
786 (user-role (hunchentoot:session-value
'user-role
))
787 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
788 (longitude (cdr (assoc :longitude data
)))
789 (latitude (cdr (assoc :latitude data
)))
790 (ellipsoid-height (cdr (assoc :ellipsoid-height data
)))
791 ;; (stdx-global (cdr (assoc :stdx-global data)))
792 ;; (stdy-global (cdr (assoc :stdy-global data)))
793 ;; (stdz-global (cdr (assoc :stdz-global data)))
794 (input-size (cdr (assoc :input-size data
)))
795 (kind (cdr (assoc :kind data
)))
796 (description (cdr (assoc :description data
)))
797 (numeric-description (cdr (assoc :numeric-description data
)))
799 (format nil
"SRID=4326; POINT(~S ~S ~S)"
800 longitude latitude ellipsoid-height
))
801 (aux-numeric-raw (cdr (assoc :aux-numeric data
)))
802 (aux-text-raw (cdr (assoc :aux-text data
)))
803 (aux-numeric (if aux-numeric-raw
804 (nullify-nil (apply #'vector aux-numeric-raw
))
806 (aux-text (if aux-text-raw
807 (nullify-nil (apply #'vector aux-text-raw
))
809 (user-point-table-name
810 (user-point-table-name presentation-project-name
)))
812 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
813 () "No write permission.")
814 (with-restarting-connection *postgresql-credentials
*
816 (= 1 (execute (:insert-into user-point-table-name
:set
819 'description description
820 'numeric-description numeric-description
821 'creation-date
'current-timestamp
822 'coordinates
(:st_geomfromewkt point-form
)
823 ;; 'stdx-global stdx-global
824 ;; 'stdy-global stdy-global
825 ;; 'stdz-global stdz-global
826 'input-size input-size
827 'aux-numeric aux-numeric
828 'aux-text aux-text
)))
829 () "No point stored. This should not happen."))))
831 (hunchentoot:define-easy-handler
832 (update-point :uri
"/phoros/lib/update-point" :default-request-type
:post
)
834 "Update point sent by user in database."
835 (assert-authentication)
836 (let* ((presentation-project-name (hunchentoot:session-value
837 'presentation-project-name
))
838 (user-id (hunchentoot:session-value
'user-id
))
839 (user-role (hunchentoot:session-value
'user-role
))
840 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
841 (user-point-id (cdr (assoc :user-point-id data
)))
842 (kind (cdr (assoc :kind data
)))
843 (description (cdr (assoc :description data
)))
844 (numeric-description (cdr (assoc :numeric-description data
)))
845 (user-point-table-name
846 (user-point-table-name presentation-project-name
)))
848 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
849 () "No write permission.")
850 (with-restarting-connection *postgresql-credentials
*
853 (:update user-point-table-name
:set
856 'description description
857 'numeric-description numeric-description
858 'creation-date
'current-timestamp
859 :where
(:and
(:= 'user-point-id user-point-id
)
860 (:or
(:= (if (string-equal user-role
871 () "No point stored. Did you try to update someone else's point ~
872 without having admin permission?"))))
874 (defun increment-numeric-string (text)
875 "Increment rightmost numeric part of text if any; otherwise append a
876 three-digit numeric part."
877 (let* ((end-of-number
878 (1+ (or (position-if #'digit-char-p text
:from-end t
)
879 (1- (length text
)))))
881 (1+ (or (position-if-not #'digit-char-p text
:from-end t
884 (width-of-number (- end-of-number start-of-number
))
885 (prefix-text (subseq text
0 start-of-number
))
886 (suffix-text (subseq text end-of-number
)))
887 (when (zerop width-of-number
)
888 (setf width-of-number
3))
889 (format nil
"~A~V,'0D~A"
892 (1+ (or (ignore-errors
895 :start start-of-number
:end end-of-number
))
899 (hunchentoot:define-easy-handler
900 (uniquify-point-attributes :uri
"/phoros/lib/uniquify-point-attributes"
901 :default-request-type
:post
)
903 "Check if received set of point-attributes are unique. If so,
904 return null; otherwise return (as a suggestion) a uniquified version
905 of point-attributes by modifying element numeric-description."
906 (assert-authentication)
907 (setf (hunchentoot:content-type
*) "application/json")
908 (let* ((presentation-project-name (hunchentoot:session-value
909 'presentation-project-name
))
910 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
911 (user-point-id (cdr (assoc :user-point-id data
)))
912 (kind (cdr (assoc :kind data
)))
913 (description (cdr (assoc :description data
)))
914 (numeric-description (cdr (assoc :numeric-description data
)))
915 (user-point-table-name
916 (user-point-table-name presentation-project-name
)))
917 (flet ((uniquep (user-point-id kind description numeric-description
)
918 "Check if given set of user-point attributes will be
927 :from user-point-table-name
928 :where
(:and
(:!= 'user-point-id user-point-id
)
930 (:= 'description description
)
931 (:= 'numeric-description
932 numeric-description
)))))
939 :from user-point-table-name
940 :where
(:and
(:= 'kind kind
)
941 (:= 'description description
)
942 (:= 'numeric-description
943 numeric-description
)))))
945 (with-restarting-connection *postgresql-credentials
*
946 (json:encode-json-to-string
948 user-point-id kind description numeric-description
)
950 for s
= numeric-description
951 then
(increment-numeric-string s
)
952 until
(uniquep user-point-id kind description s
)
954 (setf (cdr (assoc :numeric-description data
))
958 (hunchentoot:define-easy-handler
959 (delete-point :uri
"/phoros/lib/delete-point" :default-request-type
:post
)
961 "Delete user point if user is allowed to do so."
962 (assert-authentication)
963 (let* ((presentation-project-name (hunchentoot:session-value
964 'presentation-project-name
))
965 (user-id (hunchentoot:session-value
'user-id
))
966 (user-role (hunchentoot:session-value
'user-role
))
967 (user-point-table-name
968 (user-point-table-name presentation-project-name
))
969 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
))))
970 (with-restarting-connection *postgresql-credentials
*
972 (eql 1 (cond ((string-equal user-role
"admin")
973 (execute (:delete-from user-point-table-name
974 :where
(:= 'user-point-id data
))))
975 ((string-equal user-role
"write")
978 user-point-table-name
980 (:= 'user-point-id data
)
981 (:or
(:= 'user-id user-id
)
988 () "No point deleted. This should not happen."))))
990 (defun common-table-names (presentation-project-id)
991 "Return a list of common-table-names of table sets that contain data
992 of presentation project with presentation-project-id."
995 (:select
'common-table-name
997 :from
'sys-presentation
'sys-measurement
'sys-acquisition-project
999 (:= 'sys-presentation.presentation-project-id
1000 presentation-project-id
)
1001 (:= 'sys-presentation.measurement-id
1002 'sys-measurement.measurement-id
)
1003 (:= 'sys-measurement.acquisition-project-id
1004 'sys-acquisition-project.acquisition-project-id
)))
1009 "While fetching common-table-names of presentation-project-id ~D: ~A"
1010 presentation-project-id c
))))
1012 (defun encode-geojson-to-string (features &key junk-keys
)
1013 "Encode a list of property lists into a GeoJSON FeatureCollection.
1014 Each property list must contain keys for coordinates, :x, :y, :z; it
1015 may contain a numeric point :id and zero or more pieces of extra
1016 information. The extra information is stored as GeoJSON Feature
1017 properties. Exclude property list elements with keys that are in
1019 (with-output-to-string (s)
1020 (json:with-object
(s)
1021 (json:encode-object-member
:type
:*feature-collection s
)
1022 (json:as-object-member
(:features s
)
1023 (json:with-array
(s)
1025 #'(lambda (point-with-properties)
1026 (dolist (junk-key junk-keys
)
1027 (remf point-with-properties junk-key
))
1028 (destructuring-bind (&key x y z id
&allow-other-keys
) ;TODO: z probably bogus
1029 point-with-properties
1030 (json:as-array-member
(s)
1031 (json:with-object
(s)
1032 (json:encode-object-member
:type
:*feature s
)
1033 (json:as-object-member
(:geometry s
)
1034 (json:with-object
(s)
1035 (json:encode-object-member
:type
:*point s
)
1036 (json:as-object-member
(:coordinates s
)
1037 (json:encode-json
(list x y z
) s
))))
1038 (json:encode-object-member
:id id s
)
1039 (json:as-object-member
(:properties s
)
1040 (dolist (key '(:x
:y
:z
:id
))
1041 (remf point-with-properties key
))
1042 (json:encode-json-plist point-with-properties s
))))))
1044 (json:encode-object-member
:phoros-version
(phoros-version) s
))))
1047 "Return a WKT-compliant BOX3D string from string bbox."
1048 (concatenate 'string
"BOX3D("
1049 (substitute #\Space
#\
,
1050 (substitute #\Space
#\
, bbox
:count
1)
1051 :from-end t
:count
1)
1054 (hunchentoot:define-easy-handler
(points :uri
"/phoros/lib/points.json") (bbox)
1055 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
1056 (assert-authentication)
1057 (setf (hunchentoot:content-type
*) "application/json")
1059 (with-restarting-connection *postgresql-credentials
*
1060 (let* ((presentation-project-id
1061 (hunchentoot:session-value
'presentation-project-id
))
1063 (common-table-names presentation-project-id
)))
1064 (encode-geojson-to-string
1071 for common-table-name in common-table-names
1072 for aggregate-view-name
1073 = (point-data-table-name common-table-name
)
1074 ;; would have been nice, was too slow:
1075 ;; = (aggregate-view-name common-table-name)
1078 (:as
(:st_x
'coordinates
) x
)
1079 (:as
(:st_y
'coordinates
) y
)
1080 (:as
(:st_z
'coordinates
) z
)
1081 (:as
'point-id
'id
) ;becomes fid on client
1083 :distinct-on
'random
1084 :from
',aggregate-view-name
1085 :natural
:left-join
'sys-presentation
1088 (:= 'presentation-project-id
1089 ,presentation-project-id
)
1092 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
1093 ,*standard-coordinates
*))))))
1095 ,*number-of-features-per-layer
*))
1097 :junk-keys
'(:random
))))
1100 :error
"While fetching points from inside bbox ~S: ~A"
1103 (hunchentoot:define-easy-handler
1104 (aux-points :uri
"/phoros/lib/aux-points.json")
1106 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
1107 (assert-authentication)
1108 (setf (hunchentoot:content-type
*) "application/json")
1110 (let ((limit *number-of-features-per-layer
*)
1112 (aux-point-view-name (hunchentoot:session-value
1113 'presentation-project-name
))))
1114 (encode-geojson-to-string
1115 (with-restarting-connection *postgresql-aux-credentials
*
1121 (:as
(:st_x
'coordinates
) 'x
)
1122 (:as
(:st_y
'coordinates
) 'y
)
1123 (:as
(:st_z
'coordinates
) 'z
)
1124 :from
,aux-view-name
1127 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
1128 ,*standard-coordinates
*)))
1134 :error
"While fetching aux-points from inside bbox ~S: ~A"
1137 (hunchentoot:define-easy-handler
1138 (aux-local-data :uri
"/phoros/lib/aux-local-data"
1139 :default-request-type
:post
)
1141 "Receive coordinates, respond with the count nearest json objects
1142 containing arrays aux-numeric, aux-text, and distance to the
1143 coordinates received, wrapped in an array."
1144 (assert-authentication)
1145 (setf (hunchentoot:content-type
*) "application/json")
1146 (let* ((aux-view-name
1147 (aux-point-view-name (hunchentoot:session-value
1148 'presentation-project-name
)))
1149 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
1150 (longitude (cdr (assoc :longitude data
)))
1151 (latitude (cdr (assoc :latitude data
)))
1152 (count (cdr (assoc :count data
)))
1154 (format nil
"POINT(~F ~F)" longitude latitude
))
1155 (snap-distance 1e-3) ;about 100 m, TODO: make this a defparameter
1157 (format nil
"~A,~A,~A,~A"
1158 (- longitude snap-distance
)
1159 (- latitude snap-distance
)
1160 (+ longitude snap-distance
)
1161 (+ latitude snap-distance
))))
1162 (encode-geojson-to-string
1164 (with-restarting-connection *postgresql-aux-credentials
*
1171 (:as
(:st_x
'coordinates
) 'x
)
1172 (:as
(:st_y
'coordinates
) 'y
)
1173 (:as
(:st_z
'coordinates
) 'z
)
1180 ,*spherical-mercator
*)
1182 (:st_geomfromtext
,point-form
,*standard-coordinates
*)
1183 ,*spherical-mercator
*))
1185 :from
',aux-view-name
1186 :where
(:&& 'coordinates
1188 ,(box3d bounding-box
) box3d
)
1189 ,*standard-coordinates
*)))
1194 (defun nillify-null (x)
1195 "Replace occurences of :null in nested sequence x by nil."
1196 (cond ((eq :null x
) nil
)
1200 (t (map (type-of x
) #'nillify-null x
))))
1202 (defun nullify-nil (x)
1203 "Replace occurences of nil in nested sequence x by :null."
1204 (cond ((null x
) :null
)
1208 (t (map (type-of x
) #'nullify-nil x
))))
1210 (hunchentoot:define-easy-handler
1211 (aux-local-linestring :uri
"/phoros/lib/aux-local-linestring.json"
1212 :default-request-type
:post
)
1214 "Receive longitude, latitude, radius, and step-size; respond
1215 with a JSON object comprising the elements linestring (a WKT
1216 linestring stitched together of the nearest auxiliary points from
1217 within radius around coordinates), current-point (the point on
1218 linestring closest to coordinates), and previous-point and next-point
1219 \(points on linestring step-size before and after current-point
1220 respectively). Wipe away any unfinished business first."
1221 (assert-authentication)
1222 (dolist (old-thread (hunchentoot:session-value
'recent-threads
))
1224 (bt:interrupt-thread old-thread
1225 #'(lambda () (signal 'superseded
)))))
1226 (setf (hunchentoot:session-value
'recent-threads
) nil
)
1227 (setf (hunchentoot:session-value
'number-of-threads
) 1)
1228 (push (bt:current-thread
) (hunchentoot:session-value
'recent-threads
))
1229 (setf (hunchentoot:content-type
*) "application/json")
1231 (let* ((thread-aux-points-function-name
1232 (thread-aux-points-function-name (hunchentoot:session-value
1233 'presentation-project-name
)))
1234 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
1235 (longitude (cdr (assoc :longitude data
)))
1236 (latitude (cdr (assoc :latitude data
)))
1237 (radius (cdr (assoc :radius data
)))
1238 (step-size (cdr (assoc :step-size data
)))
1239 (azimuth (if (numberp (cdr (assoc :azimuth data
)))
1240 (cdr (assoc :azimuth data
))
1243 (format nil
"POINT(~F ~F)" longitude latitude
))
1246 (with-restarting-connection *postgresql-aux-credentials
*
1251 (,thread-aux-points-function-name
1253 ,point-form
,*standard-coordinates
*)
1255 ,*number-of-points-per-aux-linestring
*
1258 ,(proj:degrees-to-radians
91))))
1260 (with-output-to-string (s)
1261 (json:with-object
(s)
1262 (json:encode-object-member
1263 :linestring
(getf sql-response
:threaded-points
) s
)
1264 (json:encode-object-member
1265 :current-point
(getf sql-response
:current-point
) s
)
1266 (json:encode-object-member
1267 :previous-point
(getf sql-response
:back-point
) s
)
1268 (json:encode-object-member
1269 :next-point
(getf sql-response
:forward-point
) s
)
1270 (json:encode-object-member
1271 :azimuth
(getf sql-response
:new-azimuth
) s
))))
1273 ;; (setf (hunchentoot:return-code*) hunchentoot:+http-gateway-time-out+)
1276 (defun get-user-points (user-point-table-name &key
1277 (bounding-box "-180,-90,180,90")
1279 (order-criterion 'id
)
1281 "Return limit points from user-point-table-name in GeoJSON format,
1282 and the number of points returned."
1283 (let ((user-point-plist
1289 (:as
(:st_x
'coordinates
) 'x
)
1290 (:as
(:st_y
'coordinates
) 'y
)
1291 (:as
(:st_z
'coordinates
) 'z
)
1292 (:as
'user-point-id
'id
) ;becomes fid in OpenLayers
1293 ;; 'stdx-global 'stdy-global 'stdz-global
1295 'kind
'description
'numeric-description
1297 (:as
(:to-char
'creation-date
1298 ,*user-point-creation-date-format
*)
1300 'aux-numeric
'aux-text
1301 :from
,user-point-table-name
:natural
:left-join
'sys-user
1302 :where
(:&& 'coordinates
1303 (:st_setsrid
(:type
,(box3d bounding-box
) box3d
)
1304 ,*standard-coordinates
*)))
1311 (encode-geojson-to-string (nillify-null user-point-plist
)))
1312 (encode-geojson-to-string (nillify-null user-point-plist
)))
1313 (length user-point-plist
))))
1315 (hunchentoot:define-easy-handler
1316 (user-points :uri
"/phoros/lib/user-points.json")
1318 "Send *number-of-features-per-layer* randomly chosen GeoJSON-encoded
1319 points from inside bbox to client. If there is no bbox parameter,
1320 send all points and indent GeoJSON to make it more readable."
1321 (assert-authentication)
1322 (setf (hunchentoot:content-type
*) "application/json")
1324 (let ((bounding-box (or bbox
"-180,-90,180,90"))
1326 (limit (if bbox
*number-of-features-per-layer
* :null
))
1327 (order-criterion (if bbox
'(:random
) 'id
))
1328 (user-point-table-name
1329 (user-point-table-name (hunchentoot:session-value
1330 'presentation-project-name
))))
1331 (with-restarting-connection *postgresql-credentials
*
1332 (nth-value 0 (get-user-points user-point-table-name
1333 :bounding-box bounding-box
1335 :order-criterion order-criterion
1339 :error
"While fetching user-points~@[ from inside bbox ~S~]: ~A"
1342 (hunchentoot:define-easy-handler
1343 (user-point-attributes :uri
"/phoros/lib/user-point-attributes.json")
1345 "Send JSON object comprising arrays kinds and descriptions,
1346 each containing unique values called kind and description
1347 respectively, and count being the frequency of value in the user point
1349 (assert-authentication)
1350 (setf (hunchentoot:content-type
*) "application/json")
1352 (let ((user-point-table-name
1353 (user-point-table-name (hunchentoot:session-value
1354 'presentation-project-name
))))
1355 (with-restarting-connection *postgresql-credentials
*
1356 (with-output-to-string (s)
1357 (json:with-object
(s)
1358 (json:as-object-member
(:descriptions s
)
1359 (json:with-array
(s)
1360 (mapcar #'(lambda (x) (json:as-array-member
(s)
1361 (json:encode-json-plist x s
)))
1365 (:select
'description
1366 (:count
'description
)
1367 :from user-point-table-name
1368 :group-by
'description
)
1372 (json:as-object-member
(:kinds s
)
1373 (json:with-array
(s)
1374 (mapcar #'(lambda (x) (json:as-array-member
(s)
1375 (json:encode-json-plist x s
)))
1376 (query (format nil
"~
1377 (SELECT kind, count(kind) ~
1378 FROM ((SELECT kind FROM ~A) ~
1381 FROM (VALUES ('solitary'), ~
1384 AS defaults(kind))) ~
1385 AS kinds_union(kind) ~
1387 ORDER BY kind LIMIT 100"
1388 ;; Counts of solitary,
1389 ;; polyline, polygon may be
1390 ;; too big by one if we
1391 ;; collect them like this.
1392 (s-sql:to-sql-name user-point-table-name
))
1396 :error
"While fetching user-point-attributes: ~A"
1399 (hunchentoot:define-easy-handler photo-handler
1400 ((bayer-pattern :init-form
"65280,16711680")
1401 (color-raiser :init-form
"1,1,1")
1402 (mounting-angle :init-form
"0")
1404 "Serve an image from a .pictures file."
1405 (assert-authentication)
1409 (push (bt:current-thread
)
1410 (hunchentoot:session-value
'recent-threads
))
1411 (incf (hunchentoot:session-value
'number-of-threads
)))
1413 (cl-utilities:split-sequence
#\
/
1414 (hunchentoot:script-name
*)
1415 :remove-empty-subseqs t
))
1417 (cdddr ;remove leading phoros, lib, photo
1420 (cl-utilities:split-sequence
#\.
(first (last s
2))))
1422 (parse-integer (car (last s
)) :junk-allowed t
))
1427 :directory
(append (pathname-directory *common-root
*)
1430 :name
(first file-name-and-type
)
1431 :type
(second file-name-and-type
)))))
1433 (flex:with-output-to-sequence
(stream)
1435 stream path-to-file byte-position
1437 (apply #'vector
(mapcar
1439 (cl-utilities:split-sequence
1440 #\
, bayer-pattern
)))
1442 (apply #'vector
(mapcar
1443 #'parse-number
:parse-positive-real-number
1444 (cl-utilities:split-sequence
1447 :reversep
(= 180 (parse-integer mounting-angle
))
1448 :brightenp brightenp
))))
1449 (setf (hunchentoot:header-out
'cache-control
)
1450 (format nil
"max-age=~D" *browser-cache-max-age
*))
1451 (setf (hunchentoot:content-type
*) "image/png")
1453 (decf (hunchentoot:session-value
'number-of-threads
)))
1455 (setf (hunchentoot:return-code
*) hunchentoot
:+http-gateway-time-out
+)
1459 :error
"While serving image ~S: ~A" (hunchentoot:request-uri
*) c
))))
1461 (pushnew (hunchentoot:create-prefix-dispatcher
"/phoros/lib/photo"
1463 hunchentoot
:*dispatch-table
*)
1465 ;;; for debugging; this is the multi-file OpenLayers
1466 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1467 "/phoros/lib/openlayers/" "OpenLayers-2.10/")
1468 hunchentoot
:*dispatch-table
*)
1470 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1471 "/phoros/lib/ol/" "ol/")
1472 hunchentoot
:*dispatch-table
*)
1474 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
1475 "/phoros/lib/public_html/" "public_html/")
1476 hunchentoot
:*dispatch-table
*)
1478 (pushnew (hunchentoot:create-static-file-dispatcher-and-handler
1479 "/favicon.ico" "public_html/favicon.ico")
1480 hunchentoot
:*dispatch-table
*)
1482 (hunchentoot:define-easy-handler
1483 (view :uri
(format nil
"/phoros/lib/view-~A" (phoros-version))
1484 :default-request-type
:post
)
1486 "Serve the client their main workspace."
1488 (hunchentoot:session-value
'authenticated-p
)
1489 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
1495 "Phoros: " (hunchentoot:session-value
1496 'presentation-project-name
))))
1497 (if (cli:verbosity-level
:use-multi-file-openlayers
)
1500 :src
(format nil
"/~A/lib/openlayers/lib/Firebug/firebug.js"
1503 :src
(format nil
"/~A/lib/openlayers/lib/OpenLayers.js"
1507 :src
(format nil
"/~A/lib/ol/OpenLayers.js"
1509 (:link
:rel
"stylesheet"
1510 :href
(format nil
"/~A/lib/css-~A/style.css"
1514 (:script
:src
(format ;variability in script name is
1515 nil
; supposed to fight browser cache
1516 "/~A/lib/phoros-~A-~A-~A.js"
1519 (hunchentoot:session-value
'user-name
)
1520 (hunchentoot:session-value
'presentation-project-name
)))
1521 (:script
:src
"http://maps.google.com/maps/api/js?sensor=false"))
1524 (:noscript
(:b
(:em
"You can't do much without JavaScript here.")))
1527 "Phoros: " (who:str
(hunchentoot:session-value
'user-full-name
))
1528 (who:fmt
" (~A)" (hunchentoot:session-value
'user-name
))
1529 "with " (:span
:id
"user-role"
1530 (who:str
(hunchentoot:session-value
'user-role
)))
1532 (:span
:id
"presentation-project-name"
1533 (who:str
(hunchentoot:session-value
1534 'presentation-project-name
)))
1535 (:span
:id
"presentation-project-emptiness")
1536 (:span
:id
"recommend-fresh-login")
1537 (:span
:class
"h1-right"
1538 (:span
:id
"phoros-version"
1539 (who:fmt
"v~A" (phoros-version)))))
1540 ;; streetmap area (northwest)
1542 :class
"controlled-streetmap"
1543 (:div
:id
"streetmap" :class
"streetmap" :style
"cursor:crosshair")
1544 (:div
:id
"streetmap-controls" :class
"streetmap-controls"
1545 (:div
:id
"streetmap-vertical-strut"
1546 :class
"streetmap-vertical-strut")
1547 (:div
:id
"streetmap-layer-switcher"
1548 :class
"streetmap-layer-switcher")
1549 (:button
:id
"unselect-all-restrictions-button"
1551 :onclick
(ps-inline (unselect-all-restrictions))
1553 (:select
:id
"restriction-select"
1554 :name
"restriction-select"
1557 :onchange
(ps-inline (request-photos)))
1558 (:div
:id
"streetmap-overview" :class
"streetmap-overview")
1559 (:div
:id
"streetmap-mouse-position"
1560 :class
"streetmap-mouse-position")
1561 (:div
:id
"streetmap-zoom" :class
"streetmap-zoom")))
1562 ;; control area (north)
1564 :class
"phoros-controls" :id
"phoros-controls"
1565 (:div
:id
"real-phoros-controls"
1566 (:h2
:class
"point-creator h2-phoros-controls"
1568 (:h2
:class
"point-editor h2-phoros-controls"
1570 (:span
:id
"creator"))
1571 (:h2
:class
"point-viewer h2-phoros-controls"
1573 (:span
:id
"creator"))
1574 (:h2
:class
"aux-data-viewer h2-phoros-controls"
1575 "View Auxiliary Data")
1576 (:h2
:class
"multiple-points-viewer"
1577 "Multiple Points Selected")
1578 (:div
:class
"multiple-points-viewer"
1579 (:p
"You have selected multiple user points.")
1580 (:p
"Unselect all but one to edit or view its properties."))
1581 (:span
:class
"point-creator point-editor point-viewer"
1586 :id
"point-kind-select"
1587 :name
"point-kind-select"
1588 :class
"combobox-select write-permission-dependent"
1589 :onchange
(ps-inline
1590 (consolidate-combobox
1594 :id
"point-kind-input"
1595 :name
"point-kind-input"
1596 :class
"combobox-input write-permission-dependent"
1597 :onchange
(ps-inline
1598 (unselect-combobox-selection
1602 (:input
:id
"point-numeric-description"
1603 :class
"vanilla-input write-permission-dependent"
1605 :type
"text" :name
"point-numeric-description")
1608 :id
"point-description"
1611 :id
"point-description-select"
1612 :name
"point-description-select"
1613 :class
"combobox-select write-permission-dependent"
1614 :onchange
(ps-inline
1615 (consolidate-combobox
1616 "point-description"))
1619 :id
"point-description-input"
1620 :name
"point-description-input"
1621 :class
"combobox-input write-permission-dependent"
1622 :onchange
(ps-inline
1623 (unselect-combobox-selection
1624 "point-description"))
1627 (:button
:id
"delete-point-button" :disabled t
1629 :onclick
(ps-inline (delete-point))
1631 (:button
:disabled t
:id
"finish-point-button"
1634 (:div
:id
"uniquify-buttons"
1635 (:button
:id
"suggest-unique-button"
1638 (insert-unique-suggestion))
1640 (:button
:id
"force-duplicate-button"
1643 (:div
:id
"aux-point-distance-or-point-creation-date"
1644 (:code
:id
"point-creation-date"
1645 :class
"point-editor point-viewer")
1647 :id
"aux-point-distance" :disabled t
1648 :class
"point-creator aux-data-viewer aux-data-dependent"
1649 :size
1 :name
"aux-point-distance"
1650 :onchange
(ps-inline
1651 (aux-point-distance-selected))
1653 (enable-aux-point-selection)))
1655 :id
"include-aux-data"
1656 :class
"point-creator aux-data-dependent"
1658 (:input
:id
"include-aux-data-p"
1659 :class
"tight-input"
1660 :type
"checkbox" :checked t
1661 :name
"include-aux-data-p"
1662 :onchange
(ps-inline
1663 (flip-aux-data-inclusion)))
1665 (:div
:id
"display-nearest-aux-data"
1666 :class
"aux-data-viewer"
1668 (:input
:id
"display-nearest-aux-data-p"
1669 :class
"tight-input"
1670 :type
"checkbox" :checked t
1671 :name
"display-nearest-aux-data-p"
1672 :onchange
(ps-inline
1673 (flip-nearest-aux-data-display)))
1677 :class
"point-creator point-editor point-viewer aux-data-viewer"
1678 (:div
:id
"aux-numeric-list")
1679 (:div
:id
"aux-text-list")))
1680 (:div
:class
"walk-mode-controls"
1681 (:div
:id
"walk-mode"
1682 :class
"aux-data-dependent"
1683 (:input
:id
"walk-p"
1684 :class
"tight-input"
1685 :type
"checkbox" :checked nil
1686 :onchange
(ps-inline
1688 (:label
:for
"walk-p"
1690 (:div
:id
"decrease-step-size"
1691 :class
"aux-data-dependent"
1692 :onclick
(ps-inline (decrease-step-size)))
1693 (:div
:id
"step-size"
1694 :class
"aux-data-dependent"
1695 :onclick
(ps-inline (increase-step-size))
1697 (:div
:id
"increase-step-size"
1698 :class
"aux-data-dependent"
1699 :onclick
(ps-inline (increase-step-size))
1700 :ondblclick
(ps-inline (increase-step-size)
1701 (increase-step-size)))
1702 (:div
:id
"step-button" :disabled nil
1703 :class
"aux-data-dependent"
1704 :onclick
(ps-inline (step))
1705 :ondblclick
(ps-inline (step t
))
1707 (:div
:class
"image-main-controls"
1708 (:div
:id
"auto-zoom"
1709 (:input
:id
"zoom-to-point-p"
1710 :class
"tight-input"
1711 :type
"checkbox" :checked t
)
1712 (:label
:for
"zoom-to-point-p"
1714 (:div
:id
"brighten-images"
1715 (:input
:id
"brighten-images-p"
1716 :class
"tight-input"
1717 :type
"checkbox" :checked nil
)
1718 (:label
:for
"brighten-images-p"
1720 (:div
:id
"zoom-images-to-max-extent"
1721 :onclick
(ps-inline (zoom-images-to-max-extent)))
1722 (:div
:id
"no-footprints-p"
1724 (:div
:id
"remove-work-layers-button" :disabled t
1725 :onclick
(ps-inline (reset-layers-and-controls))
1727 ;; help area (northeast)
1731 :id
"download-user-points-button"
1733 :onclick
(format nil
1734 "self.location.href = \"/~A/lib/user-points.json\""
1736 "download points") ;TODO: offer other formats and maybe projections
1745 "/lib/blurb?openlayers-version="
1746 (@ *open-layers
*version_number
*))
1748 (:img
:src
(format nil
"/~A/lib/public_html/phoros-logo-plain.png"
1750 :alt
"Phoros" :style
"vertical-align:middle"
1752 (:button
:id
"logout-button"
1754 :onclick
(ps-inline (bye))
1756 (:h2
:id
"h2-help" "Help")
1757 (:div
:id
"help-display"))
1758 ;; image area (south)
1759 (:div
:id
"images" :style
"clear:both"
1761 for i from
0 below
*number-of-images
* do
1763 (:div
:class
"controlled-image"
1764 (:div
:id
(format nil
"image-~S-controls" i
)
1765 :class
"image-controls"
1766 (:div
:id
(format nil
"image-~S-zoom" i
)
1767 :class
"image-zoom")
1768 (:div
:id
(format nil
"image-~S-layer-switcher" i
)
1769 :class
"image-layer-switcher")
1770 (:div
:id
(format nil
"image-~S-usable" i
)
1771 :class
"image-usable"
1773 (:div
:id
(format nil
"image-~S-trigger-time" i
)
1774 :class
"image-trigger-time"))
1775 (:div
:id
(format nil
"image-~S" i
)
1776 :class
"image" :style
"cursor:crosshair"))))))))
1777 (hunchentoot:redirect
1778 (format nil
"/~A/~A"
1780 (hunchentoot:session-value
'presentation-project-name
))
1781 :add-session-id t
)))
1783 (hunchentoot:define-easy-handler
1784 (epipolar-line :uri
"/phoros/lib/epipolar-line")
1786 "Receive vector of two sets of picture parameters, the first of
1787 which containing coordinates (m, n) of a clicked point. Respond with a
1788 JSON encoded epipolar-line."
1789 (assert-authentication)
1790 (setf (hunchentoot:content-type
*) "application/json")
1791 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
))))
1792 (json:encode-json-to-string
1793 (photogrammetry :epipolar-line
(first data
) (second data
)))))
1795 (hunchentoot:define-easy-handler
1796 (estimated-positions :uri
"/phoros/lib/estimated-positions")
1798 "Receive a two-part JSON vector comprising (1) a vector containing
1799 sets of picture-parameters with clicked (\"active\") points
1800 stored in :m, :n; and (2) a vector containing sets of
1801 picture-parameters; respond with a JSON encoded two-part vector
1802 comprising (1) a point in global coordinates; and (2) a vector of
1803 image coordinates (m, n) for the global point that correspond to the
1804 images from the received second vector. TODO: report error on bad
1805 data (ex: points too far apart)."
1806 ;; TODO: global-point-for-display should probably contain a proj string in order to make sense of the (cartesian) standard deviations.
1807 (assert-authentication)
1808 (setf (hunchentoot:content-type
*) "application/json")
1810 (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
1811 (active-point-photo-parameters
1813 (number-of-active-points
1814 (length active-point-photo-parameters
))
1815 (destination-photo-parameters
1818 (cdr (assoc :cartesian-system
1819 (first active-point-photo-parameters
))))
1820 (global-point-cartesian
1822 :multi-position-intersection active-point-photo-parameters
))
1823 (global-point-geographic-radians
1824 (proj:cs2cs
(list (cdr (assoc :x-global global-point-cartesian
))
1825 (cdr (assoc :y-global global-point-cartesian
))
1826 (cdr (assoc :z-global global-point-cartesian
)))
1827 :source-cs cartesian-system
))
1828 (global-point-for-display ;points geographic cs, degrees; std deviations in cartesian cs
1829 (pairlis '(:longitude
:latitude
:ellipsoid-height
1830 ;; :stdx-global :stdy-global :stdz-global
1833 (proj:radians-to-degrees
1834 (first global-point-geographic-radians
))
1835 (proj:radians-to-degrees
1836 (second global-point-geographic-radians
))
1837 (third global-point-geographic-radians
)
1838 ;; (cdr (assoc :stdx-global global-point-cartesian))
1839 ;; (cdr (assoc :stdy-global global-point-cartesian))
1840 ;; (cdr (assoc :stdz-global global-point-cartesian))
1841 number-of-active-points
)))
1844 for i in destination-photo-parameters
1847 (photogrammetry :reprojection i global-point-cartesian
)))))
1848 (json:encode-json-to-string
1849 (list global-point-for-display image-coordinates
))))
1851 (hunchentoot:define-easy-handler
1852 (user-point-positions :uri
"/phoros/lib/user-point-positions")
1854 "Receive a two-part JSON vector comprising
1855 - a vector of user-point-id's and
1856 - a vector containing sets of picture-parameters;
1857 respond with a JSON object comprising the elements
1858 - image-points, a vector whose elements
1859 - correspond to the elements of the picture-parameters vector
1861 - are GeoJSON feature collections containing one point (in picture
1862 coordinates) for each user-point-id received;
1863 - user-point-count, the number of user-points we tried to fetch
1865 (assert-authentication)
1866 (setf (hunchentoot:content-type
*) "application/json")
1867 (with-restarting-connection *postgresql-credentials
*
1868 (let* ((user-point-table-name
1869 (user-point-table-name (hunchentoot:session-value
1870 'presentation-project-name
)))
1871 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
)))
1872 (user-point-ids (first data
))
1873 (user-point-count (length user-point-ids
))
1874 (destination-photo-parameters (second data
))
1876 (cdr (assoc :cartesian-system
1877 (first destination-photo-parameters
)))) ;TODO: in rare cases, coordinate systems of the images shown may differ
1881 (:as
(:st_x
'coordinates
) 'longitude
)
1882 (:as
(:st_y
'coordinates
) 'latitude
)
1883 (:as
(:st_z
'coordinates
) 'ellipsoid-height
)
1884 (:as
'user-point-id
'id
) ;becomes fid on client
1887 'numeric-description
1889 (:as
(:to-char
'creation-date
"IYYY-MM-DD HH24:MI:SS TZ")
1893 :from user-point-table-name
:natural
:left-join
'sys-user
1894 :where
(:in
'user-point-id
(:set user-point-ids
)))
1896 (global-points-cartesian
1898 for global-point-geographic in user-points
1900 (ignore-errors ;in case no destination-photo-parameters have been sent
1901 (pairlis '(:x-global
:y-global
:z-global
)
1904 (proj:degrees-to-radians
1905 (getf global-point-geographic
:longitude
))
1906 (proj:degrees-to-radians
1907 (getf global-point-geographic
:latitude
))
1908 (getf global-point-geographic
:ellipsoid-height
))
1909 :destination-cs cartesian-system
)))))
1912 for photo-parameter-set in destination-photo-parameters
1914 (encode-geojson-to-string
1916 for global-point-cartesian in global-points-cartesian
1917 for user-point in user-points
1919 (when (point-within-image-p
1920 (getf user-point
:id
)
1921 (hunchentoot:session-value
'presentation-project-name
)
1922 (cdr (assoc :byte-position photo-parameter-set
))
1923 (cdr (assoc :filename photo-parameter-set
))
1924 (cdr (assoc :measurement-id photo-parameter-set
)))
1926 (let ((photo-coordinates
1927 (photogrammetry :reprojection
1929 global-point-cartesian
))
1932 (setf (getf photo-point
:x
)
1933 (cdr (assoc :m photo-coordinates
)))
1934 (setf (getf photo-point
:y
)
1935 (cdr (assoc :n photo-coordinates
)))
1937 :junk-keys
'(:longitude
:latitude
:ellipsoid-height
)))))
1938 (with-output-to-string (s)
1939 (json:with-object
(s)
1940 (json:encode-object-member
:user-point-count user-point-count s
)
1941 (json:as-object-member
(:image-points s
)
1942 (json:with-array
(s)
1943 (loop for i in image-coordinates do
1944 (json:as-array-member
(s) (princ i s
))))))))))
1946 (defun point-within-image-p (user-point-id presentation-project-name
1947 byte-position filename measurement-id
)
1948 "Return t if either point with user-point-id is inside the footprint
1949 of the image described by byte-position, filename, and measurement-id;
1950 or if that image doesn't have a footprint. Return nil otherwise."
1951 (let* ((user-point-table-name (user-point-table-name
1952 presentation-project-name
))
1953 (presentation-project-id (presentation-project-id-from-name
1954 presentation-project-name
))
1955 (common-table-names (common-table-names presentation-project-id
)))
1960 for common-table-name in common-table-names
1961 for aggregate-view-name
1962 = (aggregate-view-name common-table-name
)
1966 :from
',aggregate-view-name
1967 :where
(:and
(:= 'byte-position
,byte-position
)
1968 (:= 'filename
,filename
)
1969 (:= 'measurement-id
,measurement-id
)
1970 (:or
(:is-null
'footprint
)
1972 (:select
'coordinates
1973 :from
,user-point-table-name
1974 :where
(:= 'user-point-id
1979 (hunchentoot:define-easy-handler
1980 (multi-position-intersection :uri
"/phoros/lib/intersection")
1982 "Receive vector of sets of picture parameters, respond with stuff."
1983 (assert-authentication)
1984 (setf (hunchentoot:content-type
*) "application/json")
1985 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
:force-text t
))))
1986 (json:encode-json-to-string
1987 (photogrammetry :multi-position-intersection data
))))