1 ;;; PHOROS -- Photogrammetric Road Survey
2 ;;; Copyright (C) 2010, 2011 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 *login-intro
* nil
53 "A few friendly words to be shown below the login form.")
55 (defparameter *postgresql-warnings
* nil
56 "If t, show PostgreSQL's WARNINGs and NOTICEs.")
58 (defparameter *use-multi-file-openlayers
* nil
59 "If t, use OpenLayers uncompiled from openlayers/*, which makes
60 debugging easier. Otherwise use a single-file shrunk
63 (defparameter *number-of-images
* 4
64 "Number of photos shown to the HTTP client.")
66 (defparameter *number-of-features-per-layer
* 500
67 "What we think a browser can swallow.")
69 (defparameter *number-of-points-per-aux-linestring
* 500
70 "What we think a browser can swallow.")
72 (defparameter *user-point-creation-date-format
* "IYYY-MM-DD HH24:MI:SS TZ"
73 "SQL date format used for display and GeoJSON export of user points.")
75 (defun phoros-version (&key major minor revision
)
76 "Return version of this program, either one integer part as denoted by
77 the key argument, or the whole dotted string."
78 (let* ((version-string
79 (handler-bind ((warning #'ignore-warnings
))
80 (asdf:component-version
(asdf:find-system
:phoros
))))
82 (mapcar #'parse-integer
83 (cl-utilities:split-sequence
#\. version-string
))))
84 (cond (major (first version-components
))
85 (minor (second version-components
))
86 (revision (third version-components
))
89 (defun check-dependencies ()
90 "Say OK if the necessary external dependencies are available."
93 (geographic-to-utm 33 13 52) ;check cs2cs
94 (phoros-photogrammetry:del-all
) ;check photogrammetry
95 (initialize-leap-seconds) ;check source of leap second info
96 (format *error-output
* "~&OK~%"))
97 (error (e) (format *error-output
* "~A~&" e
))))
99 (defun muffle-postgresql-warnings ()
100 "For current DB, silence PostgreSQL's warnings about implicitly
102 (unless *postgresql-warnings
*
103 (execute "SET client_min_messages TO ERROR;")))
105 (defun check-db (db-credentials)
106 "Check postgresql connection. Return t if successful; show error on
107 *error-output* otherwise. db-credentials is a list like so: (database
108 user password host &key (port 5432) use-ssl)."
111 (setf connection
(apply #'connect db-credentials
))
112 (error (e) (format *error-output
* "Database connection ~S failed: ~A~&"
115 (disconnect connection
)
118 (defun ignore-warnings (c) (declare (ignore c
)) (muffle-warning))
120 (defmethod hunchentoot:session-cookie-name
(acceptor)
121 (declare (ignore acceptor
))
124 (defun start-server (&key
(http-port 8080) address
(common-root "/"))
125 "Start the presentation project server which listens on http-port
126 at address. Address defaults to all addresses of the local machine."
127 (setf *phoros-server
*
128 (make-instance 'hunchentoot
:acceptor
131 :access-logger
#'log-http-access
132 :message-logger
#'log-hunchentoot-message
))
133 (setf hunchentoot
:*session-max-time
* (* 3600 24))
134 (setf *common-root
* common-root
)
135 (check-db *postgresql-credentials
*)
136 (with-connection *postgresql-credentials
*
137 (assert-phoros-db-major-version))
138 (hunchentoot:reset-session-secret
)
139 (hunchentoot:start
*phoros-server
*))
141 (defun stop-server () (hunchentoot:stop
*phoros-server
*))
143 (eval-when (:compile-toplevel
:load-toplevel
:execute
)
144 (register-sql-operators :2+-ary
:&& :overlaps
))
146 (setf hunchentoot
:*default-handler
*
148 "Http default response."
149 (setf (hunchentoot:return-code
*) hunchentoot
:+http-not-found
+)))
151 (hunchentoot:define-easy-handler phoros-handler
()
152 "First HTTP contact: if necessary, check credentials, establish new
154 (with-connection *postgresql-credentials
*
155 (let* ((presentation-project-name
156 (second (cl-utilities:split-sequence
157 #\
/ (hunchentoot:script-name
*) :remove-empty-subseqs t
)))
158 (presentation-project-id
161 (:select
'presentation-project-id
162 :from
'sys-presentation-project
163 :where
(:= 'presentation-project-name
164 presentation-project-name
))
167 ((null presentation-project-id
)
168 (setf (hunchentoot:return-code
*) hunchentoot
:+http-not-found
+))
169 ((and (equal (hunchentoot:session-value
'presentation-project-name
)
170 presentation-project-name
)
171 (hunchentoot:session-value
'authenticated-p
))
172 (hunchentoot:redirect
173 (format nil
"/phoros/lib/view-~A" (phoros-version))
177 (setf (hunchentoot:session-value
'presentation-project-name
)
178 presentation-project-name
)
179 (setf (hunchentoot:session-value
'presentation-project-id
)
180 presentation-project-id
)
181 (setf (hunchentoot:session-value
'presentation-project-bbox
)
184 (bounding-box (get-dao 'sys-presentation-project
185 presentation-project-name
)))))
186 (if (or (null bbox
) (eq :null bbox
))
189 (setf (hunchentoot:session-value
'aux-data-p
)
190 (with-connection *postgresql-aux-credentials
*
191 (view-exists-p (aux-point-view-name
192 presentation-project-name
))))
193 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
195 :style
"font-family:sans-serif;"
197 :method
"post" :enctype
"multipart/form-data"
198 :action
"/phoros/lib/authenticate" :name
"login-form"
200 (:legend
(:b
(:a
:href
"http://phoros.berlios.de"
201 :style
"text-decoration:none;"
203 (who:fmt
" [~A]" presentation-project-name
)))
205 (:b
(:em
"You can't do much without JavaScript there.")))
208 (:input
:type
"text" :name
"user-name"))
211 (:input
:type
"password" :name
"user-password")
213 (:span
:id
"cackle"))
214 (:input
:type
"submit" :value
"Submit"
216 (setf (chain document
217 (get-element-by-id "cackle")
219 "Ok, let's see…"))))
220 (:script
:type
"text/javascript"
221 (who:str
(ps (chain document
226 for i in
*login-intro
*
227 do
(who:htm
(:p
(who:str i
))))))))))))
229 (pushnew (hunchentoot:create-regex-dispatcher
"/phoros/(?!lib/)"
231 hunchentoot
:*dispatch-table
*)
233 (defun stored-bbox ()
234 "Return stored bounding box for user and presentation project of
236 (with-connection *postgresql-credentials
*
237 (let ((bbox (bounding-box
238 (get-dao 'sys-user-role
239 (hunchentoot:session-value
241 (hunchentoot:session-value
242 'presentation-project-id
)))))
244 (hunchentoot:session-value
'presentation-project-bbox
)
247 (defun stored-cursor ()
248 "Return stored cursor position for user and presentation project of
250 (with-connection *postgresql-credentials
*
253 (:select
(:st_x
'cursor
) (:st_y
'cursor
)
255 :where
(:and
(:= 'user-id
256 (hunchentoot:session-value
'user-id
))
257 (:= 'presentation-project-id
258 (hunchentoot:session-value
259 'presentation-project-id
))
260 (:raw
"cursor IS NOT NULL")))
263 (format nil
"~{~F~#^,~}" cursor
)))))
266 (hunchentoot:define-easy-handler
267 (authenticate-handler :uri
"/phoros/lib/authenticate"
268 :default-request-type
:post
)
270 "Check user credentials."
271 (with-connection *postgresql-credentials
*
272 (let* ((user-name (hunchentoot:post-parameter
"user-name"))
273 (user-password (hunchentoot:post-parameter
"user-password"))
274 (presentation-project-id (hunchentoot:session-value
275 'presentation-project-id
))
277 (when presentation-project-id
280 'sys-user.user-full-name
282 'sys-user-role.user-role
283 :from
'sys-user-role
'sys-user
285 (:= 'presentation-project-id presentation-project-id
)
286 (:= 'sys-user-role.user-id
'sys-user.user-id
)
287 (:= 'user-name user-name
)
288 (:= 'user-password user-password
)))
290 (user-full-name (first user-info
))
291 (user-id (second user-info
))
292 (user-role (third user-info
)))
295 (setf (hunchentoot:session-value
'authenticated-p
) t
296 (hunchentoot:session-value
'user-name
) user-name
297 (hunchentoot:session-value
'user-full-name
) user-full-name
298 (hunchentoot:session-value
'user-id
) user-id
299 (hunchentoot:session-value
'user-role
) user-role
)
300 (hunchentoot:redirect
(format nil
"/phoros/lib/view-~A"
303 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
305 :style
"font-family:sans-serif;"
307 (:a
:href
(format nil
"/phoros/~A/" (hunchentoot:session-value
308 'presentation-project-name
))
311 (hunchentoot:define-easy-handler logout-handler
(bbox longitude latitude
)
312 (if (hunchentoot:session-value
'authenticated-p
)
313 (with-connection *postgresql-credentials
*
314 (let ((presentation-project-name
315 (hunchentoot:session-value
'presentation-project-name
))
317 (get-dao 'sys-user-role
318 (hunchentoot:session-value
'user-id
)
319 (hunchentoot:session-value
'presentation-project-id
))))
322 (setf (bounding-box sys-user-role
) bbox
))
323 (when (and longitude latitude
)
324 (let* ;; kludge: should be done by some library, not by DB query
325 ((point-form (format nil
"POINT(~F ~F)" longitude latitude
))
326 (point-wkb (query (:select
327 (:st_geomfromtext point-form
))
329 (setf (cursor sys-user-role
) point-wkb
)))
330 (update-dao sys-user-role
))
331 (hunchentoot:remove-session hunchentoot
:*session
*)
332 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
338 "Phoros: logged out" )))
339 (:link
:rel
"stylesheet"
340 :href
(format nil
"/phoros/lib/css-~A/style.css"
344 (:h1
:id
"title" "Phoros: logged out")
345 (:p
"Log back in to project "
346 (:a
:href
(format nil
"/phoros/~A" presentation-project-name
)
347 (who:fmt
"~A." presentation-project-name
))))))))
350 (pushnew (hunchentoot:create-regex-dispatcher
"/logout" 'logout-handler
)
351 hunchentoot
:*dispatch-table
*)
353 (hunchentoot:define-easy-handler
354 (local-data :uri
"/phoros/lib/local-data" :default-request-type
:post
)
356 "Receive coordinates, respond with the count nearest json objects
357 containing picture url, calibration parameters, and car position,
358 wrapped in an array."
359 (when (hunchentoot:session-value
'authenticated-p
)
360 (setf (hunchentoot:content-type
*) "application/json")
361 (with-connection *postgresql-credentials
*
362 (let* ((presentation-project-id (hunchentoot:session-value
363 'presentation-project-id
))
364 (common-table-names (common-table-names
365 presentation-project-id
))
366 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
367 (longitude (cdr (assoc :longitude data
)))
368 (latitude (cdr (assoc :latitude data
)))
369 (count (cdr (assoc :count data
)))
370 (zoom (cdr (assoc :zoom data
)))
371 (snap-distance (* 10d-5
(expt 2 (- 18 zoom
)))) ; assuming geographic coordinates
372 (point-form (format nil
"POINT(~F ~F)" longitude latitude
))
381 for common-table-name in common-table-names
382 for aggregate-view-name
383 = (aggregate-view-name common-table-name
)
386 (:as
(:st_distance
'coordinates
389 ,*standard-coordinates
*))
391 'recorded-device-id
;debug
392 'device-stage-of-life-id
;debug
393 'generic-device-id
;debug
395 'filename
'byte-position
'point-id
397 ;'coordinates ;the search target
398 'longitude
'latitude
'ellipsoid-height
400 'east-sd
'north-sd
'height-sd
401 'roll
'pitch
'heading
402 'roll-sd
'pitch-sd
'heading-sd
403 'sensor-width-pix
'sensor-height-pix
'pix-size
404 'bayer-pattern
'color-raiser
406 'dx
'dy
'dz
'omega
'phi
'kappa
407 'c
'xh
'yh
'a1
'a2
'a3
'b1
'b2
'c1
'c2
'r0
408 'b-dx
'b-dy
'b-dz
'b-rotx
'b-roty
'b-rotz
409 'b-ddx
'b-ddy
'b-ddz
'b-drotx
'b-droty
'b-drotz
411 ',aggregate-view-name
413 (:and
(:= 'presentation-project-id
414 ,presentation-project-id
)
415 (:st_dwithin
'coordinates
418 ,*standard-coordinates
*)
423 (json:encode-json-to-string result
)))))
425 (hunchentoot:define-easy-handler
426 (store-point :uri
"/phoros/lib/store-point" :default-request-type
:post
)
428 "Receive point sent by user; store it into database."
429 (when (hunchentoot:session-value
'authenticated-p
)
430 (let* ((presentation-project-name (hunchentoot:session-value
431 'presentation-project-name
))
432 (user-id (hunchentoot:session-value
'user-id
))
433 (user-role (hunchentoot:session-value
'user-role
))
434 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
435 (longitude (cdr (assoc :longitude data
)))
436 (latitude (cdr (assoc :latitude data
)))
437 (ellipsoid-height (cdr (assoc :ellipsoid-height data
)))
438 (stdx-global (cdr (assoc :stdx-global data
)))
439 (stdy-global (cdr (assoc :stdy-global data
)))
440 (stdz-global (cdr (assoc :stdz-global data
)))
441 (input-size (cdr (assoc :input-size data
)))
442 (attribute (cdr (assoc :attribute data
)))
443 (description (cdr (assoc :description data
)))
444 (numeric-description (cdr (assoc :numeric-description data
)))
446 (format nil
"SRID=4326; POINT(~S ~S ~S)"
447 longitude latitude ellipsoid-height
))
448 (aux-numeric-raw (cdr (assoc :aux-numeric data
)))
449 (aux-text-raw (cdr (assoc :aux-text data
)))
450 (aux-numeric (if aux-numeric-raw
451 (apply #'vector aux-numeric-raw
)
453 (aux-text (if aux-text-raw
454 (apply #'vector aux-text-raw
)
456 (user-point-table-name
457 (user-point-table-name presentation-project-name
)))
459 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
460 () "No write permission.")
461 (with-connection *postgresql-credentials
*
463 (= 1 (execute (:insert-into user-point-table-name
:set
466 'description description
467 'numeric-description numeric-description
468 'creation-date
'current-timestamp
469 'coordinates
(:st_geomfromewkt point-form
)
470 'stdx-global stdx-global
471 'stdy-global stdy-global
472 'stdz-global stdz-global
473 'input-size input-size
474 'aux-numeric aux-numeric
475 'aux-text aux-text
)))
476 () "No point stored. This should not happen.")))))
478 (hunchentoot:define-easy-handler
479 (update-point :uri
"/phoros/lib/update-point" :default-request-type
:post
)
481 "Update point sent by user in database."
482 (when (hunchentoot:session-value
'authenticated-p
)
483 (let* ((presentation-project-name (hunchentoot:session-value
484 'presentation-project-name
))
485 (user-id (hunchentoot:session-value
'user-id
))
486 (user-role (hunchentoot:session-value
'user-role
))
487 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
488 (user-point-id (cdr (assoc :user-point-id data
)))
489 (attribute (cdr (assoc :attribute data
)))
490 (description (cdr (assoc :description data
)))
491 (numeric-description (cdr (assoc :numeric-description data
)))
492 (user-point-table-name
493 (user-point-table-name presentation-project-name
)))
495 (not (string-equal user-role
"read")) ;that is, "write" or "admin"
496 () "No write permission.")
497 (with-connection *postgresql-credentials
*
499 (= 1 (execute (:update user-point-table-name
:set
501 'description description
502 'numeric-description numeric-description
503 'creation-date
'current-timestamp
504 :where
(:and
(:= 'user-point-id user-point-id
)
505 (:= (if (string-equal user-role
510 () "No point stored. Did you try to update someone else's point ~
511 without having admin permission?")))))
513 (hunchentoot:define-easy-handler
514 (delete-point :uri
"/phoros/lib/delete-point" :default-request-type
:post
)
516 "Delete user point if user is allowed to do so."
517 (when (hunchentoot:session-value
'authenticated-p
)
518 (let* ((presentation-project-name (hunchentoot:session-value
519 'presentation-project-name
))
520 (user-id (hunchentoot:session-value
'user-id
))
521 (user-role (hunchentoot:session-value
'user-role
))
522 (user-point-table-name
523 (user-point-table-name presentation-project-name
))
524 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
525 (with-connection *postgresql-credentials
*
527 (eql 1 (cond ((string-equal user-role
"admin")
528 (execute (:delete-from user-point-table-name
529 :where
(:= 'user-point-id data
))))
530 ((string-equal user-role
"write")
531 (execute (:delete-from user-point-table-name
533 (:= 'user-point-id data
)
534 (:= 'user-id user-id
)))))))
535 () "No point deleted. This should not happen.")))))
537 (defun common-table-names (presentation-project-id)
538 "Return a list of common-table-names of table sets that contain data
539 of presentation project with presentation-project-id."
542 (:select
'common-table-name
544 :from
'sys-presentation
'sys-measurement
'sys-acquisition-project
546 (:= 'sys-presentation.presentation-project-id
547 presentation-project-id
)
548 (:= 'sys-presentation.measurement-id
549 'sys-measurement.measurement-id
)
550 (:= 'sys-measurement.acquisition-project-id
551 'sys-acquisition-project.acquisition-project-id
)))
556 "While fetching common-table-names of presentation-project-id ~D: ~A"
557 presentation-project-id c
))))
559 (defun encode-geojson-to-string (features &key junk-keys
)
560 "Encode a list of property lists into a GeoJSON FeatureCollection.
561 Each property list must contain keys for coordinates, :x, :y, :z; it
562 may contain a numeric point :id and zero or more pieces of extra
563 information. The extra information is stored as GeoJSON Feature
564 properties. Exclude property list elements with keys that are in
566 (with-output-to-string (s)
567 (json:with-object
(s)
568 (json:encode-object-member
:type
:*feature-collection s
)
569 (json:as-object-member
(:features s
)
572 #'(lambda (point-with-properties)
573 (dolist (junk-key junk-keys
)
574 (remf point-with-properties junk-key
))
575 (destructuring-bind (&key x y z id
&allow-other-keys
) ;TODO: z probably bogus
576 point-with-properties
577 (json:as-array-member
(s)
578 (json:with-object
(s)
579 (json:encode-object-member
:type
:*feature s
)
580 (json:as-object-member
(:geometry s
)
581 (json:with-object
(s)
582 (json:encode-object-member
:type
:*point s
)
583 (json:as-object-member
(:coordinates s
)
584 (json:encode-json
(list x y z
) s
))))
585 (json:encode-object-member
:id id s
)
586 (json:as-object-member
(:properties s
)
587 (dolist (key '(:x
:y
:z
:id
))
588 (remf point-with-properties key
))
589 (json:encode-json-plist point-with-properties s
))))))
591 (json:encode-object-member
:phoros-version
(phoros-version) s
))))
594 "Return a WKT-compliant BOX3D string from string bbox."
595 (concatenate 'string
"BOX3D("
596 (substitute #\Space
#\
,
597 (substitute #\Space
#\
, bbox
:count
1)
598 :from-end t
:count
1)
601 (hunchentoot:define-easy-handler
(points :uri
"/phoros/lib/points.json") (bbox)
602 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
603 (when (hunchentoot:session-value
'authenticated-p
)
604 (setf (hunchentoot:content-type
*) "application/json")
606 (with-connection *postgresql-credentials
*
607 (let* ((presentation-project-id
608 (hunchentoot:session-value
'presentation-project-id
))
610 (common-table-names presentation-project-id
)))
611 (encode-geojson-to-string
618 for common-table-name in common-table-names
619 for aggregate-view-name
620 = (point-data-table-name common-table-name
)
621 ;; would have been nice, was too slow:
622 ;; = (aggregate-view-name common-table-name)
625 (:as
(:st_x
'coordinates
) x
)
626 (:as
(:st_y
'coordinates
) y
)
627 (:as
(:st_z
'coordinates
) z
)
628 (:as
'point-id
'id
) ;becomes fid on client
631 :from
',aggregate-view-name
632 :natural
:left-join
'sys-presentation
635 (:= 'presentation-project-id
636 ,presentation-project-id
)
639 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
640 ,*standard-coordinates
*))))))
642 ,*number-of-features-per-layer
*))
644 :junk-keys
'(:random
))))
647 :error
"While fetching points from inside bbox ~S: ~A"
650 (hunchentoot:define-easy-handler
(aux-points :uri
"/phoros/lib/aux-points.json") (bbox)
651 "Send a bunch of GeoJSON-encoded points from inside bbox to client."
652 (when (hunchentoot:session-value
'authenticated-p
)
653 (setf (hunchentoot:content-type
*) "application/json")
655 (let ((limit *number-of-features-per-layer
*)
657 (aux-point-view-name (hunchentoot:session-value
658 'presentation-project-name
))))
659 (encode-geojson-to-string
660 (with-connection *postgresql-aux-credentials
*
666 (:as
(:st_x
'coordinates
) 'x
)
667 (:as
(:st_y
'coordinates
) 'y
)
668 (:as
(:st_z
'coordinates
) 'z
)
672 (:st_setsrid
(:type
,(box3d bbox
) box3d
)
673 ,*standard-coordinates
*)))
679 :error
"While fetching aux-points from inside bbox ~S: ~A"
682 (hunchentoot:define-easy-handler
683 (aux-local-data :uri
"/phoros/lib/aux-local-data"
684 :default-request-type
:post
)
686 "Receive coordinates, respond with the count nearest json objects
687 containing arrays aux-numeric, aux-text, and distance to the
688 coordinates received, wrapped in an array."
689 (when (hunchentoot:session-value
'authenticated-p
)
690 (setf (hunchentoot:content-type
*) "application/json")
691 (let* ((aux-view-name
692 (aux-point-view-name (hunchentoot:session-value
693 'presentation-project-name
)))
694 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
695 (longitude (cdr (assoc :longitude data
)))
696 (latitude (cdr (assoc :latitude data
)))
697 (count (cdr (assoc :count data
)))
699 (format nil
"POINT(~F ~F)" longitude latitude
))
700 (snap-distance 1e-3) ;about 100 m, TODO: make this a defparameter
702 (format nil
"~A,~A,~A,~A"
703 (- longitude snap-distance
)
704 (- latitude snap-distance
)
705 (+ longitude snap-distance
)
706 (+ latitude snap-distance
))))
707 (encode-geojson-to-string
709 (with-connection *postgresql-aux-credentials
*
717 (:as
(:st_x
'coordinates
) 'x
)
718 (:as
(:st_y
'coordinates
) 'y
)
719 (:as
(:st_z
'coordinates
) 'z
)
726 ,*spherical-mercator
*)
728 (:st_geomfromtext
,point-form
,*standard-coordinates
*)
729 ,*spherical-mercator
*))
731 :from
',aux-view-name
732 :where
(:&& 'coordinates
734 ,(box3d bounding-box
) box3d
)
735 ,*standard-coordinates
*)))
740 (hunchentoot:define-easy-handler
741 (aux-local-linestring :uri
"/phoros/lib/aux-local-linestring.json"
742 :default-request-type
:post
)
744 "Receive longitude, latitude, radius, and step-size; respond
745 with the a JSON object comprising the elements linestring (a WKT
746 linestring stitched together of the nearest auxiliary points from
747 within radius around coordinates), current-point (the point on
748 linestring closest to coordinates), and previous-point and next-point
749 \(points on linestring step-size before and after current-point
751 (when (hunchentoot:session-value
'authenticated-p
)
752 (setf (hunchentoot:content-type
*) "application/json")
753 (let* ((thread-aux-points-function-name
754 (thread-aux-points-function-name (hunchentoot:session-value
755 'presentation-project-name
)))
756 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
757 (longitude (cdr (assoc :longitude data
)))
758 (latitude (cdr (assoc :latitude data
)))
759 (radius (cdr (assoc :radius data
)))
760 (step-size (cdr (assoc :step-size data
)))
761 (azimuth (if (numberp (cdr (assoc :azimuth data
)))
762 (cdr (assoc :azimuth data
))
765 (format nil
"POINT(~F ~F)" longitude latitude
))
768 (with-connection *postgresql-aux-credentials
*
774 (,thread-aux-points-function-name
776 ,point-form
,*standard-coordinates
*)
778 ,*number-of-points-per-aux-linestring
*
781 ,(proj:degrees-to-radians
91))))
783 (with-output-to-string (s)
784 (json:with-object
(s)
785 (json:encode-object-member
786 :linestring
(getf sql-response
:threaded-points
) s
)
787 (json:encode-object-member
788 :current-point
(getf sql-response
:current-point
) s
)
789 (json:encode-object-member
790 :previous-point
(getf sql-response
:back-point
) s
)
791 (json:encode-object-member
792 :next-point
(getf sql-response
:forward-point
) s
)
793 (json:encode-object-member
794 :azimuth
(getf sql-response
:new-azimuth
) s
))))))
796 (defun get-user-points (user-point-table-name &key
797 (bounding-box "-180,-90,180,90")
799 (order-criterion 'id
))
800 "Return limit points from user-point-table-name in GeoJSON format,
801 and the number of points returned."
802 (let ((user-point-plist
808 (:as
(:st_x
'coordinates
) 'x
)
809 (:as
(:st_y
'coordinates
) 'y
)
810 (:as
(:st_z
'coordinates
) 'z
)
811 (:as
'user-point-id
'id
) ;becomes fid in OpenLayers
812 'stdx-global
'stdy-global
'stdz-global
814 'attribute
'description
'numeric-description
816 (:as
(:to-char
'creation-date
817 ,*user-point-creation-date-format
*)
819 'aux-numeric
'aux-text
820 :from
,user-point-table-name
:natural
:left-join
'sys-user
821 :where
(:&& 'coordinates
822 (:st_setsrid
(:type
,(box3d bounding-box
) box3d
)
823 ,*standard-coordinates
*)))
828 (encode-geojson-to-string (nsubst nil
:null user-point-plist
))
829 (length user-point-plist
))))
831 (hunchentoot:define-easy-handler
832 (user-points :uri
"/phoros/lib/user-points.json")
834 "Send *number-of-features-per-layer* randomly chosen GeoJSON-encoded
835 points from inside bbox to client. If there is no bbox parameter,
837 (when (hunchentoot:session-value
'authenticated-p
)
838 (setf (hunchentoot:content-type
*) "application/json")
840 (let ((bounding-box (or bbox
"-180,-90,180,90"))
841 (limit (if bbox
*number-of-features-per-layer
* :null
))
842 (order-criterion (if bbox
'(:random
) 'id
))
843 (user-point-table-name
844 (user-point-table-name (hunchentoot:session-value
845 'presentation-project-name
))))
846 (with-connection *postgresql-credentials
*
847 (nth-value 0 (get-user-points user-point-table-name
848 :bounding-box bounding-box
850 :order-criterion order-criterion
))))
853 :error
"While fetching user-points~@[ from inside bbox ~S~]: ~A"
856 (hunchentoot:define-easy-handler
857 (user-point-attributes :uri
"/phoros/lib/user-point-attributes.json")
859 "Send JSON object comprising arrays attributes and descriptions,
860 each containing unique values called attribute and description
861 respectively, and count being the frequency of value in the user point
863 (when (hunchentoot:session-value
'authenticated-p
)
864 (setf (hunchentoot:content-type
*) "application/json")
866 (let ((user-point-table-name
867 (user-point-table-name (hunchentoot:session-value
868 'presentation-project-name
))))
869 (with-connection *postgresql-credentials
*
870 (with-output-to-string (s)
871 (json:with-object
(s)
872 (json:as-object-member
(:descriptions s
)
874 (mapcar #'(lambda (x) (json:as-array-member
(s)
875 (json:encode-json-plist x s
)))
879 (:select
'description
880 (:count
'description
)
881 :from user-point-table-name
882 :group-by
'description
)
886 (json:as-object-member
(:attributes s
)
888 (mapcar #'(lambda (x) (json:as-array-member
(s)
889 (json:encode-json-plist x s
)))
890 (query (format nil
"~
891 (SELECT attribute, count(attribute) ~
892 FROM ((SELECT attribute FROM ~A) ~
895 FROM (VALUES ('solitary'), ~
898 AS defaults(attribute))) ~
899 AS attributes_union(attribute) ~
900 GROUP BY attribute) ~
901 ORDER BY attribute LIMIT 100"
902 ;; Counts of solitary,
903 ;; polyline, polygon may be
904 ;; to big by one if we
905 ;; collect them like this.
906 (s-sql:to-sql-name user-point-table-name
))
910 :error
"While fetching user-point-attributes: ~A"
913 (hunchentoot:define-easy-handler photo-handler
914 ((bayer-pattern :init-form
"65280,16711680")
915 (color-raiser :init-form
"1,1,1")
916 (mounting-angle :init-form
"0"))
917 "Serve an image from a .pictures file."
918 (when (hunchentoot:session-value
'authenticated-p
)
920 (let* ((s (cdr (cl-utilities:split-sequence
#\
/
921 (hunchentoot:script-name
*)
922 :remove-empty-subseqs t
)))
923 (directory (last (butlast s
2)))
924 (file-name-and-type (cl-utilities:split-sequence
925 #\.
(first (last s
2))))
926 (byte-position (parse-integer (car (last s
)) :junk-allowed t
))
931 :directory
(append (pathname-directory *common-root
*)
932 directory
'(:wild-inferiors
))
933 :name
(first file-name-and-type
)
934 :type
(second file-name-and-type
)))))
936 (setf (hunchentoot:content-type
*) "image/png")
937 (setf stream
(hunchentoot:send-headers
))
939 stream path-to-file byte-position
941 (apply #'vector
(mapcar
943 (cl-utilities:split-sequence
#\
, bayer-pattern
)))
945 (apply #'vector
(mapcar
946 #'parse-number
:parse-positive-real-number
947 (cl-utilities:split-sequence
#\
, color-raiser
)))
948 :reversep
(= 180 (parse-integer mounting-angle
))))
951 :error
"While serving image ~S: ~A" (hunchentoot:request-uri
*) c
)))))
953 (pushnew (hunchentoot:create-prefix-dispatcher
"/phoros/lib/photo"
955 hunchentoot
:*dispatch-table
*)
957 ;;; for debugging; this is the multi-file OpenLayers
958 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
959 "/phoros/lib/openlayers/" "OpenLayers-2.10/")
960 hunchentoot
:*dispatch-table
*)
962 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
"/phoros/lib/ol/" "ol/")
963 hunchentoot
:*dispatch-table
*)
965 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
966 (format nil
"/phoros/lib/css-~A/" (phoros-version)) "css/") ;TODO: merge this style.css into public_html/style.css
967 hunchentoot
:*dispatch-table
*)
969 (pushnew (hunchentoot:create-folder-dispatcher-and-handler
970 "/phoros/lib/public_html/" "public_html/")
971 hunchentoot
:*dispatch-table
*)
973 (pushnew (hunchentoot:create-static-file-dispatcher-and-handler
974 "/favicon.ico" "public_html/favicon.ico")
975 hunchentoot
:*dispatch-table
*)
977 (hunchentoot:define-easy-handler
978 (view :uri
(format nil
"/phoros/lib/view-~A" (phoros-version))
979 :default-request-type
:post
)
981 "Serve the client their main workspace."
983 (hunchentoot:session-value
'authenticated-p
)
984 (who:with-html-output-to-string
(s nil
:prologue t
:indent t
)
990 "Phoros: " (hunchentoot:session-value
991 'presentation-project-name
))))
992 (if *use-multi-file-openlayers
*
994 (:script
:src
"/phoros/lib/openlayers/lib/Firebug/firebug.js")
995 (:script
:src
"/phoros/lib/openlayers/lib/OpenLayers.js"))
996 (who:htm
(:script
:src
"/phoros/lib/ol/OpenLayers.js")))
997 (:link
:rel
"stylesheet"
998 :href
(format nil
"/phoros/lib/css-~A/style.css"
1001 (:script
:src
(format ;variability in script name is
1002 nil
; supposed to fight browser cache
1003 "/phoros/lib/phoros-~A-~A-~A.js"
1005 (hunchentoot:session-value
'user-name
)
1006 (hunchentoot:session-value
'presentation-project-name
)))
1007 (:script
:src
"http://maps.google.com/maps/api/js?sensor=false"))
1010 (:noscript
(:b
(:em
"You can't do much without JavaScript here.")))
1012 "Phoros: " (who:str
(hunchentoot:session-value
'user-full-name
))
1013 (who:fmt
" (~A)" (hunchentoot:session-value
'user-name
))
1014 "with " (:span
:id
"user-role"
1015 (who:str
(hunchentoot:session-value
'user-role
)))
1017 (:span
:id
"presentation-project-name"
1018 (who:str
(hunchentoot:session-value
1019 'presentation-project-name
)))
1020 (:span
:id
"presentation-project-emptiness")
1021 (:span
:id
"phoros-version" :class
"h1-right"
1022 (who:fmt
"v~A" (phoros-version))))
1023 (:div
:class
"controlled-streetmap"
1024 (:div
:id
"streetmap" :class
"streetmap" :style
"cursor:crosshair")
1025 (:div
:id
"streetmap-controls" :class
"streetmap-controls"
1026 (:div
:id
"streetmap-vertical-strut"
1027 :class
"streetmap-vertical-strut")
1028 (:div
:id
"streetmap-layer-switcher"
1029 :class
"streetmap-layer-switcher")
1030 (:div
:id
"streetmap-overview" :class
"streetmap-overview")
1031 (:div
:id
"streetmap-mouse-position"
1032 :class
"streetmap-mouse-position")
1033 (:div
:id
"streetmap-zoom" :class
"streetmap-zoom")))
1034 (:div
:class
"phoros-controls"
1035 (:div
:id
"real-phoros-controls"
1036 (:h2
(:span
:id
"h2-controls") (:span
:id
"creator"))
1037 (:div
:id
"point-attribute"
1039 (:select
:id
"point-attribute-select"
1040 :name
"point-attribute-select"
1041 :class
"combobox-select"
1044 (consolidate-combobox "point-attribute"))
1046 (:input
:id
"point-attribute-input"
1047 :name
"point-attribute-input"
1048 :class
"combobox-input"
1049 :onchange
(ps-inline
1050 (unselect-combobox-selection
1054 ;; (:select :id "point-attribute" :disabled t
1055 ;; :size 1 :name "point-attribute")
1056 (:input
:id
"point-numeric-description"
1057 :class
"vanilla-input"
1059 :type
"text" :name
"point-numeric-description")
1061 (:div
:id
"point-description"
1063 (:select
:id
"point-description-select"
1064 :name
"point-description-select"
1065 :class
"combobox-select"
1066 :onchange
(ps-inline
1067 (consolidate-combobox
1068 "point-description"))
1070 (:input
:id
"point-description-input"
1071 :name
"point-description-input"
1072 :class
"combobox-input"
1073 :onchange
(ps-inline
1074 (unselect-combobox-selection
1075 "point-description"))
1078 (:button
:id
"delete-point-button" :disabled t
1080 :onclick
(ps-inline (delete-point))
1082 (:button
:disabled t
:id
"finish-point-button"
1085 (:div
:id
"aux-point-distance-or-point-creation-date"
1086 (:code
:id
"point-creation-date")
1087 (:select
:id
"aux-point-distance" :disabled t
1088 :size
1 :name
"aux-point-distance"
1089 :onchange
(ps-inline
1090 (aux-point-distance-selected))
1092 (enable-aux-point-selection)))
1093 (:div
:id
"include-aux-data"
1095 (:input
:id
"include-aux-data-p"
1096 :class
"tight-input"
1097 :type
"checkbox" :checked t
1098 :name
"include-aux-data-p"
1099 :onchange
(ps-inline
1100 (flip-aux-data-inclusion)))
1102 (:div
:id
"aux-data"
1103 (:div
:id
"aux-numeric-list")
1104 (:div
:id
"aux-text-list")))
1105 (:div
:id
"multiple-points-phoros-controls"
1106 (:h2
"Multiple Points Selected")
1107 (:p
"You have selected multiple user points.")
1108 (:p
"Unselect all but one to edit or view its properties."))
1109 (:div
:class
"walk-mode-controls"
1110 (:div
:id
"walk-mode"
1112 (:input
:id
"walk-p"
1113 :class
"tight-input"
1114 :type
"checkbox" :checked nil
1115 :onchange
(ps-inline
1118 (:div
:id
"decrease-step-size"
1119 :onclick
(ps-inline (decrease-step-size)))
1120 (:div
:id
"step-size"
1121 :onclick
(ps-inline (increase-step-size))
1123 (:div
:id
"increase-step-size"
1124 :onclick
(ps-inline (increase-step-size))
1125 :ondblclick
(ps-inline (increase-step-size)
1126 (increase-step-size)))
1127 (:div
:id
"step-button" :disabled nil
1128 :onclick
(ps-inline (step))
1129 :ondblclick
(ps-inline (step t
))
1131 (:div
:class
"image-main-controls"
1132 (:div
:id
"auto-zoom"
1134 (:input
:id
"zoom-to-point-p"
1135 :class
"tight-input"
1136 :type
"checkbox" :checked t
)
1138 (:div
:id
"zoom-images-to-max-extent"
1139 :onclick
(ps-inline (zoom-images-to-max-extent)))
1140 (:div
:id
"remove-work-layers-button" :disabled t
1141 :onclick
(ps-inline (reset-layers-and-controls))
1143 (:div
:class
"help-div"
1144 (:button
:id
"download-user-points-button"
1146 :onclick
"self.location.href = \"/phoros/lib/user-points.json\""
1147 "download points") ;TODO: offer other formats and maybe projections
1148 (:button
:id
"blurb-button"
1153 (+ "/phoros/lib/blurb?openlayers-version="
1154 (@ *open-layers
*version_number
*))
1156 (:img
:src
"/phoros/lib/public_html/phoros-logo-plain.png"
1157 :alt
"Phoros" :style
"vertical-align:middle"
1159 (:button
:id
"logout-button"
1161 :onclick
(ps-inline (bye))
1163 (:h2
:id
"h2-help" "Help")
1164 (:div
:id
"help-display"))
1165 (:div
:id
"images" :style
"clear:both"
1167 for i from
0 below
*number-of-images
* do
1169 (:div
:class
"controlled-image"
1170 (:div
:id
(format nil
"image-~S-controls" i
)
1171 :class
"image-controls"
1172 (:div
:id
(format nil
"image-~S-zoom" i
)
1173 :class
"image-zoom")
1174 (:div
:id
(format nil
"image-~S-layer-switcher" i
)
1175 :class
"image-layer-switcher")
1176 (:div
:id
(format nil
"image-~S-trigger-time" i
)
1177 :class
"image-trigger-time"))
1178 (:div
:id
(format nil
"image-~S" i
)
1179 :class
"image" :style
"cursor:crosshair"))))))))
1180 (hunchentoot:redirect
1181 (concatenate 'string
"/phoros/" (hunchentoot:session-value
1182 'presentation-project-name
))
1183 :add-session-id t
)))
1185 (hunchentoot:define-easy-handler
1186 (epipolar-line :uri
"/phoros/lib/epipolar-line")
1188 "Receive vector of two sets of picture parameters, respond with
1189 JSON encoded epipolar-lines."
1190 (when (hunchentoot:session-value
'authenticated-p
)
1191 (setf (hunchentoot:content-type
*) "application/json")
1192 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
1193 (json:encode-json-to-string
1194 (photogrammetry :epipolar-line
(first data
) (second data
))))))
1196 (hunchentoot:define-easy-handler
1197 (estimated-positions :uri
"/phoros/lib/estimated-positions")
1199 "Receive a two-part JSON vector comprising (1) a vector containing
1200 sets of picture-parameters with clicked (\"active\") points
1201 stored in :m, :n; and (2) a vector containing sets of
1202 picture-parameters; respond with a JSON encoded two-part vector
1203 comprising (1) a point in global coordinates; and (2) a vector of
1204 image coordinates (m, n) for the global point that correspond to the
1205 images from the received second vector. TODO: report error on bad
1206 data (ex: points too far apart)."
1207 ;; TODO: global-point-for-display should probably contain a proj string in order to make sense of the (cartesian) standard deviations.
1208 (when (hunchentoot:session-value
'authenticated-p
)
1209 (setf (hunchentoot:content-type
*) "application/json")
1211 (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1212 (active-point-photo-parameters
1214 (number-of-active-points
1215 (length active-point-photo-parameters
))
1216 (destination-photo-parameters
1219 (cdr (assoc :cartesian-system
1220 (first active-point-photo-parameters
))))
1221 (global-point-cartesian
1223 :multi-position-intersection active-point-photo-parameters
))
1224 (global-point-geographic-radians
1225 (proj:cs2cs
(list (cdr (assoc :x-global global-point-cartesian
))
1226 (cdr (assoc :y-global global-point-cartesian
))
1227 (cdr (assoc :z-global global-point-cartesian
)))
1228 :source-cs cartesian-system
))
1229 (global-point-for-display ;points geographic cs, degrees; std deviations in cartesian cs
1230 (pairlis '(:longitude
:latitude
:ellipsoid-height
1231 :stdx-global
:stdy-global
:stdz-global
1234 (proj:radians-to-degrees
1235 (first global-point-geographic-radians
))
1236 (proj:radians-to-degrees
1237 (second global-point-geographic-radians
))
1238 (third global-point-geographic-radians
)
1239 (cdr (assoc :stdx-global global-point-cartesian
))
1240 (cdr (assoc :stdy-global global-point-cartesian
))
1241 (cdr (assoc :stdz-global global-point-cartesian
))
1242 number-of-active-points
)))
1245 for i in destination-photo-parameters
1248 (photogrammetry :reprojection i global-point-cartesian
)))))
1249 (json:encode-json-to-string
1250 (list global-point-for-display image-coordinates
)))))
1252 (hunchentoot:define-easy-handler
1253 (user-point-positions :uri
"/phoros/lib/user-point-positions")
1255 "Receive a two-part JSON vector comprising
1256 - a vector of user-point-id's and
1257 - a vector containing sets of picture-parameters;
1258 respond with a JSON object comprising the elements
1259 - image-points, a vector whose elements
1260 - correspond to the elements of the picture-parameters vector
1262 - are GeoJSON feature collections containing one point (in picture
1263 coordinates) for each user-point-id received;
1264 - user-point-count, the number of user-points we tried to fetch
1266 (when (hunchentoot:session-value
'authenticated-p
)
1267 (setf (hunchentoot:content-type
*) "application/json")
1268 (let* ((user-point-table-name
1269 (user-point-table-name (hunchentoot:session-value
1270 'presentation-project-name
)))
1271 (data (json:decode-json-from-string
(hunchentoot:raw-post-data
)))
1272 (user-point-ids (first data
))
1273 (user-point-count (length user-point-ids
))
1274 (destination-photo-parameters (second data
))
1276 (cdr (assoc :cartesian-system
1277 (first destination-photo-parameters
)))) ;TODO: in rare cases, coordinate systems of the images shown may differ
1279 (with-connection *postgresql-credentials
*
1282 (:as
(:st_x
'coordinates
) 'longitude
)
1283 (:as
(:st_y
'coordinates
) 'latitude
)
1284 (:as
(:st_z
'coordinates
) 'ellipsoid-height
)
1285 (:as
'user-point-id
'id
) ;becomes fid on client
1288 'numeric-description
1290 (:as
(:to-char
'creation-date
"IYYY-MM-DD HH24:MI:SS TZ")
1294 :from user-point-table-name
:natural
:left-join
'sys-user
1295 :where
(:in
'user-point-id
(:set user-point-ids
)))
1297 (global-points-cartesian
1299 for global-point-geographic in user-points
1301 (ignore-errors ;in case no destination-photo-parameters have been sent
1302 (pairlis '(:x-global
:y-global
:z-global
)
1305 (proj:degrees-to-radians
1306 (getf global-point-geographic
:longitude
))
1307 (proj:degrees-to-radians
1308 (getf global-point-geographic
:latitude
))
1309 (getf global-point-geographic
:ellipsoid-height
))
1310 :destination-cs cartesian-system
)))))
1313 for photo-parameter-set in destination-photo-parameters
1315 (encode-geojson-to-string
1317 for global-point-cartesian in global-points-cartesian
1318 for user-point in user-points
1321 (let ((photo-coordinates
1322 (photogrammetry :reprojection
1324 global-point-cartesian
))
1327 (setf (getf photo-point
:x
)
1328 (cdr (assoc :m photo-coordinates
)))
1329 (setf (getf photo-point
:y
)
1330 (cdr (assoc :n photo-coordinates
)))
1332 :junk-keys
'(:longitude
:latitude
:ellipsoid-height
)))))
1333 (with-output-to-string (s)
1334 (json:with-object
(s)
1335 (json:encode-object-member
:user-point-count user-point-count s
)
1336 (json:as-object-member
(:image-points s
)
1337 (json:with-array
(s)
1338 (loop for i in image-coordinates do
1339 (json:as-array-member
(s) (princ i s
))))))))))
1341 (hunchentoot:define-easy-handler
1342 (multi-position-intersection :uri
"/phoros/lib/intersection")
1344 "Receive vector of sets of picture parameters, respond with stuff."
1345 (when (hunchentoot:session-value
'authenticated-p
)
1346 (setf (hunchentoot:content-type
*) "application/json")
1347 (let* ((data (json:decode-json-from-string
(hunchentoot:raw-post-data
))))
1348 (json:encode-json-to-string
1349 (photogrammetry :multi-position-intersection data
)))))