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

Revision 3942, 16.2 kB (checked in by hans, 2 months ago)

Merge from anon-transaction-fixes-2 branch. This changeset removes
make-object and initialize-persistent-instance, makes the allocation
of object IDs simpler and more safe and removes several relicts from
previous refactoring iterations. Also, the store tests have been
extended significantly to test pathological cases and create objects
from multiple threads.

  • Property svn:eol-style set to native
  • Property svn:keywords set to author date id revision
Line 
1 (in-package :bos.m2)
2
3 (defpersistent-class allocation-area ()
4   ((active-p :accessor allocation-area-active-p :initarg :active-p)
5    (left :reader allocation-area-left :initarg :left)
6    (top :reader allocation-area-top :initarg :top)
7    (width :reader allocation-area-width :initarg :width)
8    (height :reader allocation-area-height :initarg :height)
9    (vertices :reader allocation-area-vertices :initarg :vertices)
10    (total-m2s :reader allocation-area-total-m2s)
11    (free-m2s :accessor allocation-area-free-m2s)
12    (bounding-box :transient t :reader allocation-area-bounding-box))
13   (:documentation
14    "A polygon in which to allocate meters.  LEFT, TOP, WIDTH, and
15     HEIGHT designate the bounding rectangle of the polygon.
16     VERTICES is the list of coordinates (x . y) of the polygon
17     vertices.  Initially the area is unallocated. Active
18     areas (with ACTIVE-P set) are considered for allocation
19     before inactive areas.  Inactive areas are activated
20     automatically when the previously active areas do not provide
21     enough space to meet allocation guarantees.  When such
22     activation is done, a warning message is sent, to avoid
23     running out of allocation areas."))
24
25 (defmethod print-object ((allocation-area allocation-area) stream)
26   (print-unreadable-object (allocation-area stream :type t)
27     (format stream "~a x ~a ~:[inactive~;active~] ID: ~a"
28             (allocation-area-width allocation-area)
29             (allocation-area-height allocation-area)
30             (allocation-area-active-p allocation-area)
31             (store-object-id allocation-area))))
32
33 (defmethod initialize-instance :after ((allocation-area allocation-area) &key)
34   (with-slots (total-m2s free-m2s) allocation-area
35     (setf total-m2s (calculate-total-m2-count allocation-area))
36     (setf free-m2s (- total-m2s (calculate-allocated-m2-count allocation-area))))
37   ;; FIXME probably we dont need this and should rely on *rect-publisher*
38   (dolist (tile (allocation-area-tiles allocation-area))
39     (image-tile-changed tile)))
40
41 (defmethod notify-tiles ((allocation-area allocation-area))
42   (mapc #'(lambda (tile) (image-tile-changed tile)) (allocation-area-tiles allocation-area)))
43
44 (defmethod destroy-object :before ((allocation-area allocation-area))
45   (notify-tiles allocation-area))
46
47 (defmethod initialize-transient-instance :after ((allocation-area allocation-area))
48   (notify-tiles allocation-area))
49
50 (defun compute-bounding-box (vertices)
51   "Compute the smallest bounding box of the (x . y) points in
52    VERTICES and return it as multiple values (LEFT TOP WIDTH
53    HEIGHT), chosen to be inclusive of the leftmost/topmost points
54    but exclusive (!) of the rightmost/bottommost points."
55   (let* ((left (car (elt vertices 0)))
56          (top (cdr (elt vertices 0)))
57          (right left)
58          (bottom top))
59     (loop for i from 1 below (length vertices) do
60          (let* ((v (elt vertices i))
61                 (x (car v))
62                 (y (cdr v)))
63            (setf left (min left x)
64                  right (max right x)
65                  top (min top y)
66                  bottom (max bottom y))))
67     (values left top (- right left) (- bottom top))))
68
69 (defmethod allocation-area-center ((allocation-area allocation-area))
70   (with-slots (left top width height) allocation-area
71     (list (floor (+ left (/ width 2)))
72           (floor (+ top (/ height 2))))))
73
74 (defun make-allocation-rectangle (left top width height)
75   (make-allocation-area (coerce (list (cons left top)
76                                       (cons (+ left width) top)
77                                       (cons (+ left width) (+ top height))
78                                       (cons left (+ top height)))
79                                 'vector)))
80
81 (defun make-allocation-area (vertices)
82   "Can be called like this:
83 \(make-allocation-area #((0 . 0) (0 . 10) (10 . 0)))"
84   (assert (>= (length vertices) 3))
85   (map-edges (lambda (a b)
86                (check-type (car a) integer)
87                (check-type (cdr a) integer)
88                (check-type (car b) integer)
89                (check-type (cdr b) integer)
90                ;; Kanten duerfen nicht auf einen Punkt zusammenfallen.
91                (assert (not (and (zerop (- (car a) (car b)))
92                                  (zerop (- (cdr a) (cdr b)))))
93                        nil
94                        "~a and ~a (mxm coordinates) are too close to each other ~
95                        to be considered independent polygon vertices." a b))
96              (coerce vertices 'vector))
97   ;; Punkte muessen im Vergabegebiet liegen
98   (map nil
99        (lambda (v)
100          (assert (<= 0 (car v) (1- +width+)))
101          (assert (<= 0 (cdr v) (1- +width+))))
102        vertices)
103
104   ;; Kein Punkt darf in einer anderen allocation area vorhanden sein.
105   ;; Ermangels einer polygon-Schneidefunktion iterieren wir durch alle
106   ;; Punkt der neuen allocation area.
107   (multiple-value-bind (left top width height)
108       (compute-bounding-box vertices)
109     ;; FIXME: sollte das nicht sein:
110     ;; for y from top upto (1- (+ top height)) ?
111     (loop for y from top upto (+ top height)
112        do (loop for x from left upto (+ left width)
113              when (point-in-polygon-p x y vertices)
114              do (dolist (allocation-area (class-instances 'allocation-area))
115                   (when (point-in-polygon-p x y (allocation-area-vertices allocation-area))
116                     (error "new allocation area must not intersect with existing allocation area ~A"
117                            allocation-area))))))
118
119   (make-allocation-area/unchecked vertices))
120
121 (deftransaction make-allocation-area/unchecked (vertices)
122   (multiple-value-bind (left top width height)
123       (compute-bounding-box vertices)
124     (let ((result
125            (make-instance 'allocation-area
126                           :left left
127                           :top top
128                           :width width
129                           :height height
130                           :active-p nil
131                           :vertices vertices)))
132       result)))
133
134 (defmethod allocation-area-bounding-box ((allocation-area allocation-area))
135   (with-slots (left top width height bounding-box) allocation-area
136     (unless (slot-boundp allocation-area 'bounding-box)
137       (setf bounding-box (coerce (list (cons left top)
138                                        (cons (+ left width) top)
139                                        (cons (+ left width) (+ top height))
140                                        (cons left (+ top height)))
141                                  'vector)))
142     bounding-box))
143
144 (defmethod allocation-area-bounding-box2 ((allocation-area allocation-area))
145   "Returns the bounding-box in a standard rectangle representation."
146   (with-slots (left top width height) allocation-area
147     (list left top width height)))
148
149 (defun allocation-areas-bounding-box (&optional (allocation-areas (class-instances 'allocation-area)))
150   (geometry:with-bounding-box-collect (collect)
151     (dolist (area allocation-areas)
152       (geometry:with-rectangle ((allocation-area-bounding-box2 area))
153         (collect (list left top))
154         (collect (list (1- (+ left width)) (1- (+ top height))))))))
155
156 (defun allocation-areas-plus-contracts-bounding-box ()
157   "Returns the bounding-box as with ALLOCATION-AREAS-BOUNDING-BOX, but
158 possibly augmented by any contracts that dont have an allocation-area
159 anymore."
160   (geometry:with-bounding-box-collect (collect)
161     (awhen (allocation-areas-bounding-box)
162            (geometry:with-rectangle (it)
163              (collect (list left top))
164              (collect (list (1- (+ left width)) (1- (+ top height))))))
165     (awhen (contracts-bounding-box)
166            (geometry:with-rectangle (it)
167              (collect (list left top))
168              (collect (list (1- (+ left width)) (1- (+ top height))))))))
169
170 (defun gauge (area)
171   "Liefere den Fuellpegel des Vergabegebiets (0 <= gauge <= 1)"
172   (with-slots (y top height) area
173     (/ (- y top) height)))
174
175 (defun all-allocation-areas ()
176   "Liefere alle Vergabegebiete, nach Alter sortiert."
177   (let ((unsorted (store-objects-with-class 'allocation-area)))
178     (sort (copy-list unsorted) #'< :key #'store-object-id)))
179
180 (defun active-allocation-areas ()
181   "Liefere alle aktiven Vergabegebiete, nach Alter sortiert."
182   (remove-if-not #'allocation-area-active-p (all-allocation-areas)))
183
184 (defun inactive-nonempty-allocation-areas ()
185   (remove-if-not #'(lambda (allocation-area)
186                      (not (or (allocation-area-active-p allocation-area)
187                               (null (allocation-area-free-m2s allocation-area)))))
188                  (all-allocation-areas)))
189
190 (deftransaction activate-allocation-area (area)
191   (warn "activating ~S" area)
192   (setf (slot-value area 'active-p) t)
193   (bos.m2.allocation-cache::rebuild-allocation-cache)
194   area)
195
196 (deftransaction deactivate-allocation-area (area)
197   (warn "deactivating ~S" area)
198   (setf (slot-value area 'active-p) nil)
199   (bos.m2.allocation-cache::rebuild-allocation-cache)
200   area)
201
202 ;;; FIXME can be optimized
203 (defun map-edges (fn vertices)
204   (loop
205      for i from 0 below (length vertices)
206      for a = (elt vertices (1- (length vertices))) then b
207      for b = (elt vertices i)
208      do (funcall fn a b)))
209
210 ;; http://www.ics.uci.edu/~eppstein/161/960307.html
211 (defun in-polygon-p (x y vertices)
212   (let ((c 0))
213     (map-edges (lambda (a b)
214                  (let ((x1 (car a))
215                        (y1 (cdr a))
216                        (x2 (car b))
217                        (y2 (cdr b)))
218                    (when (or (and (<= y1 y) (>  y2 y))
219                              (and (>  y1 y) (<= y2 y)))
220                      (let ((m (/ (- y y1) (- y2 y1))))
221                        (when (< x (+ x1 (* m (- x2 x1))))
222                          (incf c))))))
223                vertices)
224     (oddp c)))
225
226 (defmethod allocation-area-contracts ((allocation-area allocation-area))
227   "Return contracts within an allocation area.  XXX Only considers the first sqm of a
228 contract, so if a contract is allocated in multiple allocation areas, it may or may
229 not be returned by this function"
230   (remove-if #'(lambda (contract)
231                  (not (in-polygon-p (m2-x (first (contract-m2s contract)))
232                                     (m2-y (first (contract-m2s contract)))
233                                     (allocation-area-vertices allocation-area))))
234              (store-objects-with-class 'contract)))
235
236 (defmethod calculate-total-m2-count ((allocation-area allocation-area))
237   "Returns the total number of sqms in the allocation area (note: brute force)"
238   (with-slots (left top width height vertices) allocation-area
239     (loop for x from left upto (+ left width)
240        with retval = 0
241        do (loop for y from top upto (+ top height)
242              when (in-polygon-p x y vertices)
243              do (incf retval))
244        finally (return retval))))
245
246 (defmethod calculate-allocated-m2-count ((allocation-area allocation-area))
247   "Returns the number of sqms allocated within an allocation area"
248   (let ((retval 0))
249     (dolist (contract (store-objects-with-class 'contract))
250       (dolist (m2 (contract-m2s contract))
251         (unless m2
252           (error "contract ~A has no m2s" contract))
253         (when (in-polygon-p (m2-x m2) (m2-y m2) (allocation-area-vertices allocation-area))
254           (incf retval))))
255     retval))
256
257 (defmethod allocation-area-percent-used ((allocation-area allocation-area))
258   (/ (- (allocation-area-total-m2s allocation-area) (allocation-area-free-m2s allocation-area))
259      (/ (allocation-area-total-m2s allocation-area) 100)))
260
261 (defun tiles-crossing (left top width height)
262   (let (tiles
263         (right (* 90 (ceiling (+ left width) 90)))
264         (bottom (* 90 (ceiling (+ top height) 90))))
265     (loop for x from left upto right by 90
266        do (loop for y from top upto bottom by 90
267              do (pushnew (ensure-map-tile x y) tiles)))
268     tiles))
269
270 (defmethod allocation-area-tiles ((allocation-area allocation-area))
271   (with-slots (left top width height) allocation-area
272     (tiles-crossing left top width height)))
273
274 (defun allocation-area-consistent-p (allocation-area)
275   (let ((total (calculate-total-m2-count allocation-area))
276         (allocated (calculate-allocated-m2-count allocation-area))
277         (consistent-p t))
278     (unless (= total (allocation-area-total-m2s allocation-area))
279       (warn "~s's total count is ~d but should be ~d"
280             allocation-area (allocation-area-total-m2s allocation-area) total)
281       (setf consistent-p nil))
282     (unless (= (- total allocated) (allocation-area-free-m2s allocation-area))
283       (warn "~s's free count is ~d but should be ~d"
284             allocation-area (allocation-area-free-m2s allocation-area) (- total allocated))
285       (setf consistent-p nil))
286     consistent-p))
287
288 (defun search-adjacent (n m2 pred)
289   "Try to find N free square meters that are adjacent and that begin
290 at square meter M2.  PRED is a predicate function of one argument that
291 returns a true value if the argument specifies an allocatable square
292 meter."
293   (when (funcall pred m2)
294     (let ((allocated (make-hash-table :test #'eq))
295           (border-queue (make-queue))
296           completely-checked)
297       (labels
298           ((to-border-queue (m2)
299              (setf (gethash m2 allocated) t)
300              (enqueue m2 border-queue))
301            (try-get (x y)
302              (when (and (<= 0 x (1- +width+))
303                         (<= 0 y (1- +width+)))
304                (let ((m2 (ensure-m2 x y)))
305                  (when (and (not (gethash m2 allocated))
306                             (funcall pred m2))
307                    m2))))
308            (get-next-neighbor (m2)
309              (let ((x (m2-x m2))
310                    (y (m2-y m2)))
311                (or (try-get (1+ x) y)
312                    (try-get x (1+ y))
313                    (try-get (1- x) y)
314                    (try-get x (1- y))))))
315         (to-border-queue m2)
316         (dotimes (i (1- n)
317                   (nconc completely-checked (queue-elements border-queue)))
318           (tagbody
319            check-next
320              (if (queue-empty-p border-queue)
321                  (return nil)
322                  (let ((next (get-next-neighbor (peek-queue border-queue))))
323                    (unless next
324                      (push (dequeue border-queue) completely-checked)
325                      (go check-next))
326                    (to-border-queue next)))))))))
327
328 (defun allocate-in-area (area n)
329   (let* ((area-left (allocation-area-left area))
330          (area-top (allocation-area-top area))
331          (area-width (allocation-area-width area))
332          (area-height (allocation-area-height area))
333          (deadline (+ (get-internal-real-time)
334                       ;; give up after 10 ms
335                       (* (/ 10 1000) internal-time-units-per-second))))
336     (labels ((allocatable-p (m2)
337                (and (in-polygon-p (m2-x m2) (m2-y m2) (allocation-area-vertices area))
338                     (not (m2-contract m2)))))
339       (loop
340          (let* ((x (+ area-left (random area-width)))
341                 (y (+ area-top (random area-height)))
342                 (m2 (ensure-m2 x y))
343                 (result (search-adjacent n m2 #'allocatable-p)))
344            (when result
345              (assert (alexandria:setp result :test #'equal))
346              (assert (= n (length result)))
347              (return result))
348            (when (> (get-internal-real-time) deadline)
349              (return nil)))))))
350
351 (defun allocate-m2s-for-sale (n)
352   "The main entry point to the allocation machinery. Will return a
353 list of N m2 instances or NIL if the requested amount cannot be
354 allocated. As a second value, returns the corresponding
355 allocation-area.
356
357 The returned m2s are still free and (decf (allocation-area-free-m2s
358 area) n) has not yet happened."
359   (alexandria:nth-value-or 0
360     (bos.m2.allocation-cache:find-exact-match n :remove t)
361     (dolist (area (active-allocation-areas))
362       (when (<= n (allocation-area-free-m2s area))
363         (let ((m2s (allocate-in-area area n)))
364           (when m2s
365             (return (values m2s area))))))
366     (dolist (area (inactive-nonempty-allocation-areas))
367       (when (<= n (allocation-area-free-m2s area))
368         (let ((m2s (allocate-in-area area n)))
369           (when m2s
370             (activate-allocation-area area)
371             (return (values m2s area))))))))
372
373 (defgeneric return-contract-m2s (m2s)
374   (:documentation "Mark the given square meters as free, so that
375     they can be re-allocated."))
376
377 (defmethod return-contract-m2s (m2s)
378   (loop for m2 in m2s
379      for allocation-area = (m2-allocation-area m2)
380      when allocation-area
381      do (incf (allocation-area-free-m2s allocation-area))))
Note: See TracBrowser for help on using the browser.