root/trunk/projects/bos/m2/m2.lisp

Revision 4104, 33.8 kB (checked in by hans, 3 days ago)

Refactor sat-tree handler.
Move to new JSON object serialization API.
Experiment with satellite image in JS.

  • Property svn:eol-style set to native
  • Property svn:keywords set to author date id revision
Line 
1 (in-package :bos.m2)
2
3 ;;;
4 (defun get-map-tile (x y)
5   (get-tile (m2-store-tile-index *m2-store*) x y))
6
7 (defun ensure-map-tile (x y)
8   (ensure-tile (m2-store-tile-index *m2-store*) x y))
9
10 ;;;; M2
11
12 ;;; Exportierte Funktionen:
13 ;;;
14 ;;; M2-CONTRACT (m2) => contract or NIL
15 ;;; M2-NUM (m2) => integer
16 ;;; M2-PRINTABLE (m2) => string
17 ;;; M2-X (m2) => integer
18 ;;; M2-Y (m2) => integer
19 ;;; M2-UTM-X (m2) => double-float
20 ;;; M2-UTM-y (m2) => double-float
21 ;;;
22 ;;; GET-M2 (x y) => m2 or NIL
23 ;;; ENSURE-M2 (x y) => m2
24 ;;; GET-M2-WITH-NUM (sqm-num) => m2 or nil
25 ;;; ENSURE-M2-WITH-NUM (sqm-num) => m2
26
27 (define-persistent-class m2 ()
28   ((x :read)
29    (y :read)
30    (contract :update :relaxed-object-reference t)
31    (my-slot :read))
32   (:default-initargs :contract nil)
33   (:class-indices (m2-index :index-type tiled-index
34                             :slots (x y)
35                             :index-reader m2-at
36                             :index-initargs (:width +width+
37                                              :height +width+
38                                              :tile-size +m2tile-width+
39                                              :tile-class 'image-tile))))
40
41 (defmethod print-object ((m2 m2) stream)
42   (if (and (slot-boundp m2 'x)
43            (slot-boundp m2 'y)
44            (slot-boundp m2 'contract))
45       (print-unreadable-object (m2 stream :type t :identity nil)
46         (format stream "at (~D,~D), ~A"
47                 (m2-x m2)
48                 (m2-y m2)
49                 (if (m2-contract m2) "sold" "free")))
50       (print-unreadable-object (m2 stream :type t :identity t)
51         (format stream "(unbound slots)"))))
52
53 (defun get-m2 (&rest coords)
54   (m2-at coords))
55
56 (defun ensure-m2 (&rest coords)
57   (or (m2-at coords)
58       (destructuring-bind (x y) coords
59         (make-instance 'm2 :x x :y y))))
60
61 (defmethod get-m2-with-num ((num integer))
62   (multiple-value-bind (y x) (truncate num +width+)
63     (get-m2 x y)))
64
65 (defmethod get-m2-with-num ((num string))
66   (get-m2-with-num (parse-integer num :radix 36)))
67
68 (defmethod ensure-m2-with-num ((num integer))
69   (multiple-value-bind (y x) (truncate num +width+)
70     (ensure-m2 x y)))
71
72 (defmethod ensure-m2-with-num ((num string))
73   (ensure-m2-with-num (parse-integer num :radix 36)))
74
75 (defun m2-num (m2)
76   "Fortlaufende Quadratmeternummer in row-major-order."
77   (+ (* (m2-y m2) +width+) (m2-x m2)))
78
79 (defun m2-num-string (m2)
80   "Quadratmeternummer im druckbaren Format (Radix 36, 6 Zeichen lang)"
81   (format nil "~36,6,'0R" (m2-num m2)))
82
83 ;; UTM laeuft von links nach rechts und von UNTEN NACH OBEN.
84 (defun m2-utm-x (m2) (+ +nw-utm-x+ (m2-x m2)))
85 (defun m2-utm-y (m2) (- +nw-utm-y+ (m2-y m2)))
86 (defun m2-utm (m2) (list (m2-utm-x m2) (m2-utm-y m2)))
87
88 (defun m2-lon-lat (m2)
89   (geo-utm:utm-x-y-to-lon-lat (m2-utm-x m2) (m2-utm-y m2) +utm-zone+ t))
90
91 (defmethod m2-num-to-utm ((num integer))
92   (multiple-value-bind (y x) (truncate num +width+)
93     (+ +nw-utm-x+ x)
94     (- +nw-utm-y+ y)))
95
96 (defmethod m2-num-to-utm ((num string))
97   (m2-num-to-utm (parse-integer num :radix 36)))
98
99 (defmethod m2-allocation-area ((m2 m2))
100   (find-if #'(lambda (allocation-area) (point-in-polygon-p (m2-x m2) (m2-y m2) (allocation-area-vertices allocation-area)))
101            (class-instances 'allocation-area)))
102
103 (defun m2s-polygon (m2s)
104   (let* ((m2 (first m2s))
105          (contract (m2-contract m2)))
106     (region-to-polygon (list (m2-x m2) (m2-y m2))
107                        (lambda (p)
108                          (let ((m2 (apply #'get-m2 p)))
109                            (and m2 (eql contract (m2-contract m2))))))))
110
111 (defun m2s-polygon-lon-lat (m2s)
112   (let ((polygon (m2s-polygon m2s)))
113     (mapcar (lambda (point)
114               (destructuring-bind (x y) point
115                 (geo-utm:utm-x-y-to-lon-lat (+ +nw-utm-x+ x) (- +nw-utm-y+ y) +utm-zone+ t)))
116             polygon)))
117
118 (defun m2s-connected-p (m2s)
119   "Is this region of m2 objects geographically connected? We do
120   not care about associated contracts or anything else."
121   (labels ((m2-neighbours (m2)
122              (let ((x (m2-x m2))
123                    (y (m2-y m2)))
124                (delete-if (lambda (m2) (not (member m2 m2s)))
125                           (list (get-m2 (1- x) y)
126                                 (get-m2 (1+ x) y)
127                                 (get-m2 x      (1- y))
128                                 (get-m2 x      (1+ y)))))))
129     (geometry:nodes-connected-p m2s
130                                 #'m2-neighbours
131                                 #'eq)))
132
133 ;;;; SPONSOR
134
135 ;;; Exportierte Funktionen:
136 ;;;
137 ;;; MAKE-SPONSOR (&rest initargs) => sponsor
138 ;;; (Automatisch Zuweisung eines Login-Namens.)
139 ;;;
140 ;;; SPONSOR-PASSWORD-QUESTION (sponsor) => string
141 ;;; SPONSOR-PASSWORD-ANSWER (sponsor) => string
142 ;;; SPONSOR-INFO-TEXT (sponsor) => string
143 ;;; SPONSOR-COUNTRY (sponsor) => string
144 ;;; SPONSOR-LANGUAGE (sponsor) => string (preferred language)
145 ;;; SPONSOR-CONTRACTS (sponsor) => list of contract
146 ;;;
147 ;;; Sowie Funktionen von USER.
148
149 (define-persistent-class sponsor (user)
150   ((master-code :read :initform nil)
151    (info-text :update :initform nil)
152    (country :update :initform nil)
153    (contracts :update :initform nil)
154    (language :update :initform nil))
155   (:default-initargs :full-name nil :email nil))
156
157 (defmethod user-editable-p ((sponsor sponsor))
158   nil)
159
160 (defun sponsor-p (object)
161   (equal (class-of object) (find-class 'sponsor)))
162
163 (deftransaction sponsor-set-info-text (sponsor newval)
164   (setf (sponsor-info-text sponsor) newval))
165
166 (deftransaction sponsor-set-country (sponsor newval)
167   (setf (sponsor-country sponsor) newval))
168
169 (deftransaction sponsor-set-language (sponsor newval)
170   (setf (sponsor-language sponsor) newval))
171
172 (defmethod sponsor-language :around ((sponsor sponsor))
173   (or (call-next-method)
174       "en"))
175
176 (defun sponsor-paid-contracts (sponsor)
177   (remove-if-not #'contract-paidp (sponsor-contracts sponsor)))
178
179 (defvar *sponsor-counter-lock* (bknr.datastore::mp-make-lock "Sponsor Counter Lock"))
180
181 (defvar *sponsor-counter* 0)
182
183 (defun next-sponsor-counter ()
184   "Return a unique number to use when generating a sponsor.
185   Uniqueness is guaranteed only across the running time of the process."
186   (bknr.datastore::mp-with-lock-held (*sponsor-counter-lock*)
187     (incf *sponsor-counter*)))
188
189 (defun make-sponsor (&rest initargs &key login &allow-other-keys)
190   (apply #'make-instance 'sponsor
191          :login (or login (format nil "s-~36R-~36R" (next-sponsor-counter) (get-universal-time)))
192          :master-code (mod (+ (get-universal-time) (random 1000000)) 1000000)
193          initargs))
194
195 (defun sponsor-consistent-p (sponsor)
196   (labels ((contract-points-to-sponsor (contract)
197              (eq sponsor (contract-sponsor contract))))
198     (let ((consistent t))
199       (unless (every #'contract-points-to-sponsor (sponsor-contracts sponsor))
200         (let ((*print-length* 5))
201           (warn "~s of ~s dont point to it by CONTRACT-SPONSOR~
202                  ~%the wrongly pointed to objs with duplicates removed are: ~s"
203                 (remove-if #'contract-points-to-sponsor (sponsor-contracts sponsor))
204                 sponsor
205                 (remove-duplicates (remove sponsor (mapcar #'contract-sponsor (sponsor-contracts sponsor))))))
206         (setq consistent nil))
207       consistent)))
208
209 (defmethod destroy-object :before ((sponsor sponsor))
210   (mapc #'delete-object (sponsor-contracts sponsor)))
211
212 (defmethod sponsor-id ((sponsor sponsor))
213   (store-object-id sponsor))
214
215 (define-user-flag :editor)
216
217 (defmethod editor-p ((user user))
218   (or (admin-p user)
219       (user-has-flag user :editor)))
220
221 (defmethod editor-p ((user null))
222   nil)
223
224 (defclass editor-only-handler ()
225   ())
226
227 (defmethod bknr.web:authorized-p ((handler editor-only-handler))
228   (editor-p (bknr.web:bknr-session-user)))
229
230 ;;;; CONTRACT
231
232 ;;; Exportierte Funktionen:
233 ;;;
234 ;;; MAKE-CONTRACT (sponsor m2s) => contract
235 ;;;
236 ;;; GET-CONTRACT (id) => contract
237 ;;;
238 ;;; CONTRACT-SPONSOR (contract) => sponsor
239 ;;; CONTRACT-PAIDP (contract) => boolean
240 ;;; CONTRACT-DATE (contract) => Universal-Timestamp
241 ;;; CONTRACT-M2S (contract) => list of m2
242 ;;; CONTRACT-BOUNDING-BOX (contract) => (list left top width height)
243 ;;;
244 ;;; CONTRACT-SET-PAIDP (contract newval) => newval
245
246 (defvar *claim-colors* '((0 0 128)
247                          (0 128 0)
248                          (0 128 128)
249                          (128 0 0)
250                          (128 0 128)
251                          (128 128 0)
252                          (0 0 255)
253                          (0 255 0)
254                          (0 255 255)
255                          (255 0 0)
256                          (255 0 255)
257                          (255 255 0)))
258
259 (define-persistent-class contract ()
260   ((sponsor :read :relaxed-object-reference t)
261    (date :read)
262    (paidp :update)
263    (m2s :read)
264    (color :read)
265    (download-only :update)
266    (cert-issued :read)
267    (worldpay-trans-id :update :initform nil)
268    (expires :read :documentation "universal time which specifies the
269      time the contract expires (is deleted) when it has not been paid for"
270                   :initform nil)
271    (largest-rectangle :update))
272   (:default-initargs
273       :m2s nil
274     :paidp nil
275     :download-only nil
276     :color (random-elt *claim-colors*)
277     :cert-issued nil
278     :expires (+ (get-universal-time) *manual-contract-expiry-time*)))
279
280 (defmethod print-object ((object contract) stream)
281   (print-unreadable-object (object stream :type t :identity nil)
282     (format stream "ID: ~D, ~A"
283             (store-object-id object)
284             (if (contract-paidp object) "paid" "unpaid"))))
285
286 (defun contract-p (object)
287   (equal (class-of object) (find-class 'contract)))
288
289 (defmethod initialize-instance :after ((contract contract) &key area)
290   (pushnew contract (sponsor-contracts (contract-sponsor contract)))
291   (dolist (m2 (contract-m2s contract))
292     (setf (m2-contract m2) contract)
293     (decf (allocation-area-free-m2s area)))
294   (setf (contract-largest-rectangle contract)
295         (contract-compute-largest-rectangle contract))
296   (publish-contract-change contract))
297
298 (defmethod destroy-object :before ((contract contract))
299   (let ((sponsor (contract-sponsor contract)))
300     (when sponsor
301       (setf (sponsor-contracts sponsor) (remove contract (sponsor-contracts sponsor)))))
302   (publish-contract-change contract :type 'delete)
303   (return-contract-m2s (contract-m2s contract))
304   (dolist (m2 (contract-m2s contract))
305     (setf (m2-contract m2) nil)))
306
307 (defun get-contract (id)
308   (let ((contract (store-object-with-id id)))
309     (prog1
310         contract
311       (unless (subtypep (type-of contract) 'contract)
312         (error "invalid contract id (wrong type) ~A" id)))))
313
314 (defun publish-contract-change (contract &key type)
315   (publish-rect-change *rect-publisher* (contract-bounding-box contract) contract :type type))
316
317 (defmethod contract-is-expired ((contract contract))
318   (and (contract-expires contract)
319        (> (get-universal-time) (contract-expires contract))))
320
321 (deftransaction contract-set-paidp (contract newval)
322   (setf (contract-paidp contract) newval)
323   (publish-contract-change contract)
324   (add-to-contract-stats contract)
325   (bknr.rss::add-item "news" contract))
326
327 (defmethod contract-price ((contract contract))
328   (* (length (contract-m2s contract)) +price-per-m2+))
329
330 (defmethod contract-download-only-p ((contract contract))
331   (contract-download-only contract))
332
333 (deftransaction contract-set-download-only-p (contract newval)
334   (setf (contract-download-only contract) newval))
335
336 (defmethod (setf contract-download-only) :around (newval (obj contract))
337   "Ensures that NEWVAL is either T or NIL."
338   (call-next-method (if newval t nil) obj))
339
340 (defmethod contract-fdf-pathname ((contract contract) &key language print)
341   (when (and print
342              (contract-download-only-p contract))
343     (error "no print fdf for download-only contract ~A" contract))
344   (merge-pathnames (make-pathname :name (format nil "~D-~(~A~)"
345                                                 (store-object-id contract)
346                                                 language)
347                                   :type "fdf")
348                    (if print *cert-mail-directory* *cert-download-directory*)))
349
350 (defmethod contract-m2-pdf-pathname ((contract contract) &key print)
351   (merge-pathnames (make-pathname :name (format nil "~D-m2s" (store-object-id contract))
352                                   :type "pdf")
353                    (if print bos.m2::*cert-mail-directory* bos.m2::*cert-download-directory*)))
354
355 (defmethod contract-pdf-pathname ((contract contract) &key print)
356   (merge-pathnames (make-pathname :name (format nil "~D" (store-object-id contract))
357                                   :type "pdf")
358                    (if print bos.m2::*cert-mail-directory* bos.m2::*cert-download-directory*)))
359
360 (defmethod contract-pdf-url ((contract contract))
361   (format nil "/certificate/~A" (store-object-id contract)))
362
363 (defmethod contract-certificates-generated-p (contract)
364   (probe-file (contract-pdf-pathname contract)))
365
366 (defmethod contract-delete-certificate-files (contract)
367   (ignore-errors
368     (delete-file (contract-pdf-pathname contract))
369     (delete-file (contract-pdf-pathname contract :print t))))
370
371 (defmethod contract-issue-cert ((contract contract) name &key address language)
372   (when (contract-cert-issued contract)
373     (warn "re-issuing cert for ~A" contract))
374   (contract-delete-certificate-files contract)
375   (make-certificate contract name :address address :language language)
376   (when (and (equal language "de")
377              (not (contract-download-only-p contract)))
378     (make-certificate contract name :address address :language language :print t))
379   (change-slot-values contract 'cert-issued t))
380
381 (defmethod contract-image-tiles ((contract contract))
382   (let (image-tiles)
383     (dolist (m2 (contract-m2s contract))
384       (pushnew (get-map-tile (m2-x m2) (m2-y m2))
385                image-tiles))
386     image-tiles))
387
388 (defmethod contract-bounding-box ((contract contract))
389   (geometry:with-bounding-box-collect (collect)
390     (dolist (m2 (contract-m2s contract))
391       (collect (list (m2-x m2) (m2-y m2))))))
392
393 (defun all-contracts ()
394   "Return list of all contracts in the system."
395   (class-instances 'contract))
396
397 (defun contracts-bounding-box (&optional (contracts (all-contracts)))
398   (geometry:with-bounding-box-collect (collect)
399     (dolist (contract contracts)
400       (dolist (m2 (contract-m2s contract))
401         (collect (list (m2-x m2) (m2-y m2)))))))
402
403 (defun contract-area (contract)
404   (length (contract-m2s contract)))
405
406 (defun contract-polygon (contract)
407   (m2s-polygon (contract-m2s contract)))
408
409 (defun contract-compute-largest-rectangle (contract)
410   (macrolet ((when-scaling-needed (arg &body body)
411                `(if (= scaler 1)
412                     ,arg
413                     (progn ,@body))))
414     (let* ((m2s (contract-m2s contract))
415            (area (length m2s))
416            (scaler (ceiling area 1000.0))
417            (bounding-box (contract-bounding-box contract))
418            (bounding-width (third bounding-box))
419            (bounding-height (fourth bounding-box)))
420       (if (= area (* bounding-width bounding-height))
421           ;; no need to run screamer here, since we already know the
422           ;; answer
423           bounding-box
424           (geometry:with-rectangle bounding-box
425             (declare (ignore width height))
426             (labels ( ;; to-orig
427                      (distance-to-orig (d)
428                        (when-scaling-needed d
429                                             (round (* d scaler))))
430                      (x-coordinate-to-orig (x)
431                        (when-scaling-needed x
432                                             (+ left (round (* (- x left) scaler)))))
433                      (y-coordinate-to-orig (y)
434                        (when-scaling-needed y
435                                             (+ top (round (* (- y top) scaler)))))
436                      (rectangle-to-orig (r)
437                        (when-scaling-needed r
438                                             (geometry:with-rectangle r
439                                               (list (x-coordinate-to-orig left)
440                                                     (y-coordinate-to-orig top)
441                                                     (distance-to-orig width)
442                                                     (distance-to-orig height)))))
443                      ;; from-orig
444                      (distance-from-orig (d)
445                        (when-scaling-needed d
446                                             (floor d scaler)))
447                      (x-coordinate-from-orig (x)
448                        (when-scaling-needed x
449                                             (+ left (floor (- x left) scaler))))
450                      (y-coordinate-from-orig (y)
451                        (when-scaling-needed y
452                                             (+ top (floor (- y top) scaler))))
453                      (rectangle-from-orig (r)
454                        (when-scaling-needed r
455                                             (geometry:with-rectangle r
456                                               (list (x-coordinate-from-orig left)
457                                                     (y-coordinate-from-orig top)
458                                                     (distance-from-orig width)
459                                                     (distance-from-orig height))))))
460               (rectangle-to-orig
461                (screamer-user:largest-rectangle
462                 (rectangle-from-orig bounding-box)
463                 (lambda (x y)
464                   (let ((m2 (get-m2 (x-coordinate-to-orig x) (y-coordinate-to-orig y))))
465                     (and m2 (eql contract (m2-contract m2)))))))))))))
466
467 (defun contract-neighbours (contract)
468   "Return all contracts that have an adjacent m2 to one of CONTRACT's m2s.
469 Note that this function takes also diagonally connected m2s into account."
470   (let (contracts)
471     (flet ((push-neighbour (x y)
472              (let ((m2 (get-m2 x y)))
473                (when (and m2
474                           (m2-contract m2)
475                           (not (eq (m2-contract m2) contract)))
476                  (pushnew (m2-contract m2) contracts)))))
477       (dolist (m2 (contract-m2s contract) contracts)
478         (let ((x (m2-x m2))
479               (y (m2-y m2)))
480           (push-neighbour (1- x) y)
481           (push-neighbour x (1- y))
482           (push-neighbour (1+ x) y)
483           (push-neighbour x (1+ y))
484           (push-neighbour (1- x) (1+ y))
485           (push-neighbour (1- x) (1- y))
486           (push-neighbour (1+ x) (1+ y))
487           (push-neighbour (1+ x) (1- y)))))))
488
489 (defun contract-center (contract)
490   (destructuring-bind (left top width height)
491       (contract-largest-rectangle contract)
492     (rectangle-center (list left top width height) :roundp nil)))
493
494 (defun contract-center-lon-lat (contract)
495   (error "this function is deprecated")
496   (let ((center (contract-center contract)))
497     (with-points (center)
498       (geo-utm:utm-x-y-to-lon-lat (+ +nw-utm-x+ center-x) (- +nw-utm-y+ center-y) +utm-zone+ t))))
499
500 (define-condition allocation-areas-exhausted (simple-error)
501   ((numsqm :initarg :numsqm :reader numsqm))
502   (:report (lambda (condition stream)
503              (format stream "Could not satisfy your request for ~A sqms, please contact the BOS office"
504                      (numsqm condition)))))
505
506 (defvar *make-contract-lock* (bt:make-lock "make-contract-lock"))
507
508 (defun make-contract (sponsor m2-count
509                       &key (date (get-universal-time))
510                       paidp
511                       (expires (+ (get-universal-time) *manual-contract-expiry-time*))
512                       download-only)
513   (unless (and (integerp m2-count)
514                (plusp m2-count))
515     (error "number of square meters must be a positive integer"))
516   (bt:with-lock-held (*make-contract-lock*)
517     (multiple-value-bind (m2s area)
518         (allocate-m2s-for-sale m2-count)
519       (unless m2s
520         (warn "can't create contract, ~A square meters for ~A could not be allocated" m2-count sponsor)
521         (send-system-mail :subject "Contact creation failed - Allocation areas exhaused"
522                           :text (format nil "A contract for ~A square meters could not be created, presumably because no
523 suitable allocation area was found.  Please check the free allocation
524 areas and add more space.
525
526 Sponsor-ID: ~A
527 "
528                                         m2-count (store-object-id sponsor)))
529         (error 'allocation-areas-exhausted :numsqm m2-count))
530       (make-instance 'contract
531                      :sponsor sponsor
532                      :date date
533                      :m2s m2s
534                      :area area
535                      :expires expires
536                      :download-only download-only
537                      :paidp paidp))))
538
539 (deftransaction recolorize-contracts (&optional colors)
540   "Assigns a new color to each contract choosing from COLORS, so
541 that CONTRACTS-WELL-COLORED-P holds."
542   (assert (consistent-p))
543   (let ((contracts (class-instances 'contract)))
544     (loop for contract in contracts
545        for color in (screamer-user:colorize colors contracts #'contract-neighbours)
546        do (setf (slot-value contract 'color) color)
547        do (publish-contract-change contract))))
548
549 (defun contracts-well-colored-p ()
550   "Checks if all contracts have a different color than all their
551 neighbours."
552   (loop for contract in (class-instances 'contract)
553      do (when (member (contract-color contract) (contract-neighbours contract)
554                       :key #'contract-color :test #'equal)
555           (return nil))
556      finally (return t)))
557
558 (defun contract-consistent-p (contract)
559   (labels ((m2-points-to-contract (m2)
560              (eq contract (m2-contract m2)))
561            (get-contract-m2 (x y)
562              (let ((m2 (get-m2 x y)))
563                (when (and m2 (m2-points-to-contract m2))
564                  m2)))
565            (m2-neighbours (m2)
566              (let ((x (m2-x m2))
567                    (y (m2-y m2)))
568                (delete nil
569                        (list (get-contract-m2 (1- x) y)
570                              (get-contract-m2 (1+ x) y)
571                              (get-contract-m2 x      (1- y))
572                              (get-contract-m2 x      (1+ y))))))
573            (contract-connected-p (contract)
574              (geometry:nodes-connected-p (contract-m2s contract)
575                                          #'m2-neighbours
576                                          #'eq)))
577     (let ((consistent t))
578       (unless (member contract (sponsor-contracts (contract-sponsor contract)))
579         (warn "~s has a sponsor ~s, but is not a member of SPONSOR-CONTRACTS, which is ~s"
580               contract (contract-sponsor contract)
581               (sponsor-contracts (contract-sponsor contract)))
582         (setq consistent nil))
583       (unless (every #'m2-points-to-contract (contract-m2s contract))
584         (let ((*print-length* 5))
585           (warn "~s of ~s dont point to it by M2-CONTRACT~
586                  ~%either those m2s are free or point to another contract~
587                  ~%the wrongly pointed to objs with duplicates removed are: ~s"
588                 (remove-if #'m2-points-to-contract (contract-m2s contract))
589                 contract
590                 (remove-duplicates (remove contract (mapcar #'m2-contract (contract-m2s contract))))))
591         (setq consistent nil))
592       (when (null (contract-m2s contract))
593         (warn "~s has no m2s" contract)
594         (setq consistent nil))
595       (when (not (contract-connected-p contract))
596         (warn "~s has m2s that are not connected" contract)
597         (setq consistent nil))
598       consistent)))
599
600 (defun contract-published-p (contract)
601   "Determines whether CONTRACT should be visible in the sat-app or in GE."
602   ;; this is a new function - there might still be places that
603   ;; use only CONTRACT-PAIDP, but mean CONTRACT-PUBLISHED-P
604   (contract-paidp contract))
605
606 ;;; contract-stats
607 (defconstant +last-contracts-cache-size+ 20)
608 (defvar *contract-stats*)
609
610 (defstruct country-stat
611   (sold-m2s 0)
612   (paying-sponsors 0))
613
614 (defstruct contract-stats
615   (sold-m2s 0)
616   (paying-sponsors 0)
617   (country-sponsors (make-hash-table))
618   (last-contracts (make-list +last-contracts-cache-size+)))
619
620 (defun initialize-contract-stats ()
621   (setq *contract-stats* (make-contract-stats))
622   (dolist (contract (remove-if-not #'contract-paidp (class-instances 'contract)))
623     (add-to-contract-stats contract)))
624
625 (defun add-to-contract-stats (contract)
626   (let* ((area (contract-area contract))
627          (sponsor (contract-sponsor contract))
628          (new-sponsor-p (alexandria:length= 1 (sponsor-contracts sponsor))))
629     (with-slots (sold-m2s paying-sponsors country-sponsors last-contracts)
630         *contract-stats*
631       ;; sold-m2s
632       (incf sold-m2s area)
633       ;; paying-sponsors
634       (when new-sponsor-p
635         (incf paying-sponsors))
636       ;; country-sponsors
637       (when (sponsor-country sponsor)
638         (let* ((country (make-keyword-from-string (sponsor-country sponsor)))
639                (country-stat (gethash country country-sponsors)))
640           (unless country-stat
641             (setq country-stat (setf (gethash country country-sponsors) (make-country-stat))))
642           (when new-sponsor-p
643             (incf (country-stat-paying-sponsors country-stat)))
644           (incf (country-stat-sold-m2s country-stat) area)))
645       ;; last-contracts
646       (setf last-contracts (nbutlast last-contracts))
647       (push contract last-contracts))))
648
649 (defun number-of-sold-sqm ()
650   (contract-stats-sold-m2s *contract-stats*))
651
652 (defun number-of-paying-sponsors ()
653   (contract-stats-paying-sponsors *contract-stats*))
654
655 (defun contract-stats-for-country (country)
656   (assert (keywordp country))
657   (let ((stat (gethash country (contract-stats-country-sponsors *contract-stats*))))
658     (if stat
659         (values (country-stat-paying-sponsors stat)
660                 (country-stat-sold-m2s stat))
661         (values 0 0))))
662
663 (defun last-paid-contracts ()
664   (remove-if (lambda (contract)
665                (or (null contract)
666                    (object-destroyed-p contract)))
667              (contract-stats-last-contracts *contract-stats*)))
668
669 (defun invoke-with-countries (function)
670   (alexandria:maphash-keys function (contract-stats-country-sponsors *contract-stats*)))
671
672 (defmacro do-sponsor-countries ((country) &body body)
673   (check-type country symbol)
674   `(invoke-with-countries (lambda (,country) ,@body)))
675
676 (register-transient-init-function 'initialize-contract-stats)
677
678 (defun string-safe (string)
679   (if string
680       (escape-nl (arnesi:escape-as-html string))
681       ""))
682
683 (defun make-m2-javascript (sponsor)
684   "Erzeugt das Quadratmeter-Javascript fÃŒr die angegebenen Contracts"
685   (with-output-to-string (*standard-output*)
686     (format t "profil = {};~%")
687     (format t "profil.id = ~D;~%" (store-object-id sponsor))
688     (format t "profil.name = ~S;~%" (string-safe (or (user-full-name sponsor) "[anonym]")))
689     (format t "profil.country = ~S;~%" (or (sponsor-country sponsor) "[unbekannt]"))
690     (format t "profil.anzahl = ~D;~%" (loop for contract in (sponsor-paid-contracts sponsor)
691                                          sum (length (contract-m2s contract))))
692     (format t "profil.nachricht = \"~A\";~%" (string-safe (sponsor-info-text sponsor)))
693     (format t "profil.contracts = [ ];~%")
694     (dolist (contract (sponsor-paid-contracts sponsor))
695       (destructuring-bind (left top width height) (contract-bounding-box contract)
696         (format t "profil.contracts.push({ id: ~A, left: ~A, top: ~A, width: ~A, height: ~A, date: ~S });~%"
697                 (store-object-id contract)
698                 left top width height
699                 (format-date-time (contract-date contract) :show-time nil))))))
700
701 (defmethod json:encode-slots progn ((contract contract))
702   (destructuring-bind (left top width height) (contract-bounding-box contract)
703     (json:encode-object-elements
704      "timestamp" (format-date-time (contract-date contract) :mail-style t)
705      "count" (length (contract-m2s contract))
706      "top" top
707      "left" left
708      "width" width
709      "height" height)))
710
711 (defmethod json:encode-slots progn ((sponsor sponsor))
712   (json:encode-object-elements
713    "name" (user-full-name sponsor)
714    "country" (or (sponsor-country sponsor) "sponsor-country-unknown")
715    "infoText" (sponsor-info-text sponsor))
716   (unless (user-full-name sponsor)
717     (json:encode-object-element "anonymous" t))
718   (json:with-object-element ("contracts")
719     (json:with-array ()
720       (dolist (contract (sponsor-paid-contracts sponsor))
721         (json:encode-object contract)))))
722
723 (defun sponsors-as-json (sponsors)
724   "Render the SPONSORS as JSON"
725   (json:with-array ()
726     (dolist (sponsor sponsors)
727       (json:encode-object sponsor))))
728
729 (defun delete-directory (pathname)
730   (cl-fad:delete-directory-and-files pathname :if-does-not-exist :ignore))
731
732 (defun reinit (&key delete directory website-url enable-mails)
733   (format t "~&; Startup Quadratmeterdatenbank...~%")
734   (force-output)
735   (setf *enable-mails* enable-mails)
736   (setf *website-url* website-url)
737   (unless directory
738     (error ":DIRECTORY parameter not set in m2.rc"))
739   (assert (and (null (pathname-name directory))
740                (null (pathname-type directory)))
741           (directory)
742           ":DIRECTORY parameter is ~s (not a directory pathname)" directory)
743   (when delete
744     (delete-directory directory)
745     (assert (not (probe-file directory))))
746   (close-store)
747   (make-instance 'm2-store
748                  :directory directory
749                  :subsystems (list (make-instance 'store-object-subsystem)
750                                    (make-instance 'blob-subsystem
751                                                   :n-blobs-per-directory 1000)
752                                    (make-instance 'initialization-subsystem)))
753   (format t "~&; Startup der Quadratmeterdatenbank done.~%")
754   (force-output))
755
756 (defun consistent-p ()
757   (let ((inconsistent-objs
758          (list
759           (remove-if #'sponsor-consistent-p (class-instances 'sponsor))
760           (remove-if #'contract-consistent-p (class-instances 'contract))
761           (remove-if #'allocation-area-consistent-p (class-instances 'allocation-area)))))
762     (values (every #'null inconsistent-objs)
763             inconsistent-objs)))
764
765 ;; testing
766
767 (defun fill-with-random-contracts (&optional percentage)
768   (loop for sponsor = (make-sponsor)
769      while (and (or (null percentage)
770                     (< (allocation-area-percent-used (first (class-instances 'allocation-area))) percentage))
771                 (make-contract sponsor
772                                (random-elt (cons (1+ (random 300))
773                                                  '(1 1 1 1 1 5 5 10 10 10 10 10 10 10 10
774                                                    10 10 10 10 10 30 30 30)))
775                                :paidp t))))
776
777
778 ;;; for quick visualization
779 #+ltk
780 (defun show-m2s-polygon (m2s &aux (points (m2s-polygon m2s)))
781   (labels ((compute-bounding-box (m2s)
782              (let* ((left (m2-x (elt m2s 0)))
783                     (top (m2-y (elt m2s 0)))
784                     (right left)
785                     (bottom top))
786                (loop for i from 1 below (length m2s) do
787                     (let* ((v (elt m2s i))
788                            (x (m2-x v))
789                            (y (m2-y v)))
790                       (setf left (min left x)
791                             right (max right x)
792                             top (min top y)
793                             bottom (max bottom y))))
794                (values left top (- right left) (- bottom top)))))
795     (multiple-value-bind (left top width height)
796         (compute-bounding-box m2s)
797       (declare (ignore width height))
798       (finish-output)
799       (flet ((transform-x (x)
800                (+ 30 (* 30 (- x left))))
801              (transform-y (y)
802                (+ 30 (* 30 (- y top)))))
803         (ltk:with-ltk ()
804           (let ((canvas (make-instance 'ltk:canvas :width 700 :height 700)))
805             ;; draw m2s
806             (loop for m2 in m2s
807                for x = (transform-x (m2-x m2))
808                for y = (transform-y (m2-y m2))
809                do (ltk:create-text canvas (+ 10 x) (+ 10 y) "x"))
810             ;; draw polygon
811             (loop for a in points
812                for b in (cdr points)
813                while (and a b)
814                do (ltk:create-line* canvas
815                                     (transform-x (first a)) (transform-y (second a))
816                                     (transform-x (first b)) (transform-y (second b))))
817             (let ((a (first points)))
818               (ltk:create-text canvas (transform-x (first a)) (transform-y (second a)) "o"))
819             (ltk:pack canvas)))))))
820
821 #+ltk
822 (defun show-contract-center (contract)
823   (labels ((compute-bounding-box (m2s)
824              (let* ((left (m2-x (elt m2s 0)))
825                     (top (m2-y (elt m2s 0)))
826                     (right left)
827                     (bottom top))
828                (loop for i from 1 below (length m2s) do
829                     (let* ((v (elt m2s i))
830                            (x (m2-x v))
831                            (y (m2-y v)))
832                       (setf left (min left x)
833                             right (max right x)
834                             top (min top y)
835                             bottom (max bottom y))))
836                (values left top (- right left) (- bottom top)))))
837     (let* ((m2s (contract-m2s contract))
838            (rectangle (contract-largest-rectangle contract))
839            (center (geometry:rectangle-center rectangle)))
840       (multiple-value-bind (left top width height)
841           (compute-bounding-box m2s)
842         (declare (ignore width height))
843         (finish-output)
844         (flet ((transform-x (x)
845                  (+ 30 (* 30 (- x left))))
846                (transform-y (y)
847                  (+ 30 (* 30 (- y top)))))
848           (ltk:with-ltk ()
849             (let ((canvas (make-instance 'ltk:canvas :width 700 :height 700)))
850               ;; draw m2s
851               (loop for m2 in m2s
852                  for x = (transform-x (m2-x m2))
853                  for y = (transform-y (m2-y m2))
854                  do (ltk:create-text canvas (+ 10 x) (+ 10 y) "x"))
855               (geometry:with-rectangle rectangle
856                 (ltk:create-rectangle canvas (transform-x left) (transform-y top)
857                                       (transform-x (+ left width)) (transform-y (+ top height))))
858               (destructuring-bind (x y)
859                   center
860                 (geometry:with-rectangle ((list (- x 0.1) (- y 0.1) 0.2 0.2))
861                   (ltk:create-rectangle canvas (transform-x left) (transform-y top)
862                                         (transform-x (+ left width)) (transform-y (+ top height)))))
863               (ltk:pack canvas))))))))
Note: See TracBrowser for help on using the browser.