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

Revision 3671, 17.3 kB (checked in by ksprotte, 4 months ago)

again whitespace cleanup + removed tabs

Line 
1 (in-package :geometry)
2
3 ;; a point in this package is represented
4 ;; as a list (x y)
5
6 ;; a rectangle is represented
7 ;; as a list (left top width height)
8
9 (defmacro with-point (point &body body)
10   (let* ((*package* (symbol-package point))
11          (x (intern (format nil "~A-X" (symbol-name point))))
12          (y (intern (format nil "~A-Y" (symbol-name point)))))
13     `(destructuring-bind (,x ,y) ,point
14        ,@body)))
15
16 (defmacro with-points ((&rest points) &body body)
17   (if (null points)
18       `(progn ,@body)
19       `(with-point ,(car points)
20          (with-points (,@(cdr points))
21            ,@body))))
22
23 (defmacro with-rectangle (rectangle-or-options &body body)
24   (destructuring-bind (rectangle &key suffix) (ensure-list rectangle-or-options)
25     (flet ((add-suffix (symbol)
26              (if suffix
27                  (intern (format nil "~a-~a" (symbol-name symbol) (string-upcase suffix)))
28                  (intern (symbol-name symbol)))))
29       `(destructuring-bind (,(add-suffix 'left)
30                             ,(add-suffix 'top)
31                              ,(add-suffix 'width)
32                              ,(add-suffix 'height))
33            ,rectangle
34          ,@body))))
35
36 (defun distance (point-a point-b)
37   (with-points (point-a point-b)
38     (sqrt (+ (expt (- point-a-x point-b-x) 2)
39              (expt (- point-a-y point-b-y) 2)))))
40
41 (defmacro dorect ((point (left top width height) &key test row-change) &body body)
42   "Iterate with POINT over all points in rect row per row. The list
43 containing x and y is intended for only extracting those
44 and not to be stored away (it will be modified).
45
46 BODY is only executed, if TEST of the current point is true.
47
48 For convenience, a null arg function ROW-CHANGE can be given
49 that will be called between the rows."
50   (check-type point symbol)
51   (rebinding (left top)
52     `(iter
53        (with ,point = (list nil nil))
54        (for y from ,top to (1- (+ ,top ,height)))
55        ,(when row-change
56               `(unless (first-time-p)
57                  (funcall ,row-change)))
58        (iter
59          (for x from ,left to (1- (+ ,left ,width)))
60          (setf (first ,point) x
61                (second ,point) y)
62          (when ,(if test
63                     `(funcall ,test ,point)
64                     t)
65            ,@body)))))
66
67 (defun rectangle-center (rectangle &key roundp)
68   (with-rectangle rectangle
69     (let ((x (+ left (/ width 2)))
70           (y (+ top (/ height 2))))
71       (if roundp
72           (list (round x) (round y))
73           (list x y)))))
74
75 (defun rectangle-intersects-p (a b)
76   (with-rectangle (a :suffix a)
77     (with-rectangle (b :suffix b)
78       (let* ((right-a (+ left-a width-a))
79              (bottom-a (+ top-a height-a))
80              (right-b (+ left-b width-b))
81              (bottom-b (+ top-b height-b))
82              (left (max left-a left-b))
83              (top (max top-a top-b))
84              (right (min right-a right-b))
85              (bottom (min bottom-a bottom-b)))
86         (and (> right left) (> bottom top))))))
87
88 ;; maybe change this function to take a
89 ;; point as an argument?
90 (defun point-in-polygon-p (x y polygon)
91   (let (result
92         (py y))
93     (loop with (pjx . pjy) = (aref polygon (1- (length polygon)))
94        for (pix . piy) across polygon
95        when (and (or (and (<= piy py) (< py pjy))
96                      (and (<= pjy py) (< py piy)))
97                  (< x
98                     (+ (/ (* (- pjx pix) (- py piy))
99                           (- pjy piy))
100                        pix)))
101        do (setf result (not result))
102        do (setf pjx pix
103                 pjy piy))
104     result))
105
106 (defun point-in-circle-p (point center radius)
107   (<= (distance point center) radius))
108
109 (defun point-in-rect-p (point rectangle)
110   (with-point point
111     (with-rectangle rectangle
112       (and (<= left point-x)
113            (< point-x (+ left width))
114            (<= top point-y)
115            (< point-y (+ top height))))))
116
117 ;;; for fun...
118 ;; (defun point-in-circle-p-test ()
119 ;;   (let ((center (list 4 4)))
120 ;;     (dorect (p (0 0 10 10) :row-change #'terpri)
121 ;;       (if (point-in-circle-p p center 3)
122 ;;        (princ "x")
123 ;;        (princ ".")))))
124
125 (defun bounding-box (objects &key (key #'identity))
126   (let (min-x min-y max-x max-y)
127     (dolist (obj objects)
128       (let ((point (funcall key obj)))
129         (with-point point
130           (setf min-x (min point-x (or min-x point-x)))
131           (setf min-y (min point-y (or min-y point-y)))
132           (setf max-x (max point-x (or max-x point-x)))
133           (setf max-y (max point-y (or max-y point-y))))))
134     (list min-x min-y (1+ (- max-x min-x)) (1+ (- max-y min-y)))))
135
136 (defmacro with-bounding-box-collect ((collect) &body body)
137   `(let (min-x min-y max-x max-y)
138      (flet ((,collect (point)
139               (with-point point
140                 (setf min-x (min point-x (or min-x point-x)))
141                 (setf min-y (min point-y (or min-y point-y)))
142                 (setf max-x (max point-x (or max-x point-x)))
143                 (setf max-y (max point-y (or max-y point-y))))))
144        ,@body)
145      (when min-x
146        (list min-x min-y (1+ (- max-x min-x)) (1+ (- max-y min-y))))))
147
148 ;;; directions
149
150 ;; A direction can be represented either
151 ;; as one of the symbols:
152 ;; :down, :left, :right, :up
153 ;;
154 ;; or as a list of dx and dy
155 ;; which can be used to move from one
156 ;; point to another in that direction
157 ;;
158 ;; the mapping is as follows:
159 ;;
160 ;;  dx  dy    symbol
161 ;;  --  --    -----
162 ;;  0   1     :down
163 ;; -1   0     :left
164 ;;  1   0     :right
165 ;;  0  -1     :up
166 ;;
167
168 (defmethod turn-right ((direction symbol))
169   (case direction
170     (:down :left)
171     (:left :up)
172     (:up :right)
173     (:right :down)))
174
175 (defmethod turn-right ((direction list))
176   (direction-as-list (turn-right (direction-as-symbol direction))))
177
178 (defmethod turn-left ((direction symbol))
179   (case direction
180     (:down :right)
181     (:right :up)
182     (:up :left)
183     (:left :down)))
184
185 (defmethod turn-left ((direction list))
186   (direction-as-list (turn-left (direction-as-symbol direction))))
187
188 (defmethod direction-as-symbol ((direction symbol))
189   direction)
190
191 (defmethod direction-as-symbol ((direction list))
192   (arnesi:switch (direction :test #'equal)
193     (((0 1)) :down)
194     (((-1 0)) :left)
195     (((1 0)) :right)
196     (((0 -1)) :up)))
197
198 (defmethod direction-as-list ((direction list))
199   direction)
200
201 (defmethod direction-as-list ((direction symbol))
202   (case direction
203     (:down '(0 1))
204     (:left '(-1 0))
205     (:right '(1 0))
206     (:up '(0 -1))))
207
208 (defmethod move ((point list) direction)
209   (destructuring-bind (x y)
210       point
211     (destructuring-bind (dx dy)
212         (direction-as-list direction)
213       (list (+ x dx)
214             (+ y dy)))))
215
216 ;;; TODO add eql for directions ?
217
218 (defun find-boundary-point (point in-region-p &optional (direction :up))
219   (let* ((direction (direction-as-list direction))
220          (next (move point direction)))
221     (if (funcall in-region-p next)
222         (find-boundary-point next in-region-p)
223         point)))
224
225 ;;; region-to-polygon
226 (defun region-to-polygon (point in-region-p)
227   "Will return a closed path of points in mathematical order.
228 IN-REGION-P is a predicate that takes a point as an argument.
229 It defines the region whose bounding polygon is to be found."
230   (let ((polygon)
231         (count 0)
232         (boundary-point (find-boundary-point point in-region-p :up))
233         (initial-direction :left))
234     (labels ((neighbour (point direction)
235                "Validate the NEIGHBOUR of POINT in DIRECTION,
236               if it is part of the region, returns (NEIGHBOUR DIRECTION),
237               otherwise returns NIL."
238                (when point
239                  (let ((neighbour (move point direction)))
240                    (when (funcall in-region-p neighbour)
241                      (list neighbour direction)))))
242              (choose-next (point direction)
243                "Returns a place to move to next as a list (NEXT-POINT NEXT-DIRECTION).
244                 NEXT-POINT can be the same POINT (but then with a different direction."
245                (acond
246                 ((neighbour point (turn-right direction)) it)
247                 ((neighbour (first (neighbour point direction))
248                             (turn-right direction))
249                  it)
250                 ((neighbour point direction) it)
251                 (t (list point (turn-left direction)))))
252              (terminate (point direction)
253                "Are we done?"
254                (when (and (eql direction initial-direction)
255                           (equal point boundary-point))
256                  (incf count)
257                  (= 2 count)))
258              (push-point (point direction)
259                "Add a point to POLYGON. The actual point
260                 depends on the DIRECTION."
261                (push
262                 (case direction
263                   (:left point)
264                   (:down (move point :down))
265                   (:right (move (move point :down) :right))
266                   (:up (move point :right)))
267                 polygon))
268              (traverse (point direction)
269                "Go to next POINT by DIRECTION."
270                (push-point point direction)
271                (unless (terminate point direction)
272                  (destructuring-bind (next-point next-direction)
273                      (choose-next point direction)
274                    (traverse next-point next-direction)))))
275       (traverse boundary-point initial-direction)
276       (nreverse polygon))))
277
278
279 ;;; formatting
280 ;; proposed by Michael Weber on alexandria-devel
281 (defun format-mixed-radix-number (stream number radix-list format-list
282                                   &key lsb-first leading-zeros
283                                   (trailing-zeros t))
284   "Prints NUMBER to STREAM in mixed-radix RADIX.
285 representation-LIST is a list of radixes, least-significant first.
286 FORMAT-LIST is a list of format directives, one for each digit.
287 When LSB-FIRST is nil (default), print most-significant digit first,
288 otherwise least-significant digit first.
289 When LEADING-ZEROS and TRAILING-ZEROS are nil, leading and
290 trailing zero digits are not printed, respectively. \(default: remove
291 leading zeros, keep trailing zeros)"
292   (let ((format-pairs
293          (loop with digit and fraction
294             initially (setf (values number fraction)
295                             (truncate number))
296             for f-list on format-list
297             and r-list = radix-list then (rest r-list)
298             collect (list (first f-list)
299                           (cond ((endp r-list)
300                                  (shiftf number 0))
301                                 ((rest f-list)
302                                  (setf (values number digit)
303                                        (truncate number (first r-list)))
304                                  digit)
305                                 (t number)))
306             into list
307             finally (progn
308                       (incf (cadar list) fraction)
309                       (return (nreverse list))))))
310     (unless trailing-zeros
311       (setf format-pairs (member-if #'plusp format-pairs :key
312                                     #'second)))
313     (when lsb-first
314       (setf format-pairs (nreverse format-pairs)))
315     (unless leading-zeros
316       (setf format-pairs (member-if #'plusp format-pairs :key
317                                     #'second)))
318     (format stream "~{~{~@?~}~}" format-pairs)))
319
320
321 (defun format-decimal-degree (degree)
322   (format-mixed-radix-number nil (* 60 60 degree) '(60 60 360) '("~,2FÂŽÂŽ" "~DÂŽ" "~D°")))
323
324 (defun format-lon-lat (stream lon lat)
325   (format stream "~A~:[S~;N~], ~A~:[W~;E~]"
326           (format-decimal-degree (abs lat))
327           (plusp lat)
328           (format-decimal-degree (abs lon))
329           (plusp lon)))
330
331 ;;; publish - subscribe on rectangles
332
333 ;;; rect-publisher
334 (defvar *rect-publisher*)
335
336 (defun make-rect-publisher ()
337   "MAKE-RECT-PUBLISHER creates a new publisher object."
338   (setf *rect-publisher* (%make-rect-publisher)))
339
340 (defstruct (rect-publisher (:constructor %make-rect-publisher))
341   subscribers)
342
343 (defstruct rect-subscriber
344   object rectangle callback-fn)
345
346 (defun register-rect-subscriber (publisher subscriber rectangle callback-fn)
347   "Register SUBSCRIBER with associated RECTANGLE and CALLBACK-FN with
348 PUBLISHER, so that on changes in RECTANGLE, CALLBACK-FN will be called
349 with SUBSCRIBER and the published INFO as additional args."
350   (remove-rect-subscriber publisher subscriber)
351   (push (make-rect-subscriber :object subscriber :rectangle (copy-list rectangle) :callback-fn callback-fn)
352         (rect-publisher-subscribers publisher))
353   subscriber)
354
355 (defun remove-rect-subscriber (publisher subscriber)
356   "Unsubscribes SUBSCRIBER from PUBLISHER."
357   (setf (rect-publisher-subscribers publisher)
358         (delete subscriber (rect-publisher-subscribers publisher)
359                 :key #'rect-subscriber-object)))
360
361 (defun publish-rect-change (publisher rectangle &rest info)
362   "Tells PUBLISHER about changes in RECTANGLE. All subscribers whose
363 own rectangle intersects with RECTANGLE will be notified. The kind of
364 change can be further specified by INFO."
365   (dolist (subscriber (rect-publisher-subscribers publisher))
366     (when (rectangle-intersects-p rectangle (rect-subscriber-rectangle subscriber))
367       ;; (print (rect-subscriber-callback-fn subscriber))
368       (apply (rect-subscriber-callback-fn subscriber) (rect-subscriber-object subscriber) info))))
369
370
371 (in-package :screamer-user)
372
373 (export 'largest-rectangle)
374 (defun largest-rectangle (bounding-rectangle in-region-p)
375   "Returns the largest rectangle inside a region (a polygon), which is
376 specified here by its BOUNDING-RECTANGLE and the predicate IN-REGION-P
377 that will be called with two arguments X and Y to determine if a given
378 point belongs to the region or not."
379   (destructuring-bind (l tt w h)
380       bounding-rectangle
381     (let ((left (an-integer-betweenv l (1- (+ l w)) 'left))
382           (top (an-integer-betweenv tt (1- (+ tt h)) 'top))
383           (width (an-integer-betweenv 1 w 'width))
384           (height (an-integer-betweenv 1 h 'height))
385           (right (an-integer-betweenv (1+ l) (+ l w) 'right))
386           (bottom (an-integer-betweenv (1+ tt) (+ tt h) 'bottom))
387           (area (an-integer-betweenv 1 (* w h) 'area)))
388       (assert! (=v width (-v right left)))
389       (assert! (=v height (-v bottom top)))
390       (assert! (=v area (*v width height)))
391       (assert! (funcallv #'(lambda (left top right bottom)
392                              (block result
393                                (loop for x from left below right
394                                   do (loop for y from top below bottom
395                                         do (unless (funcall in-region-p x y) (return-from result nil))))
396                                (return-from result t)))
397                          left top right bottom))
398       ;; (best-value (solution (list left top width height) (reorder #'range-size (constantly nil) #'< #'linear-force)) area)
399       (first (best-value (solution (list left top width height) (static-ordering #'linear-force)) area)))))
400
401 (defun integer-random-force (variable)
402   (let ((variable (value-of variable)))
403     (when (screamer::variable? variable)
404       (screamer::restrict-value!
405        variable
406        (cond ((not (eq (screamer::variable-enumerated-domain variable) t))
407               (a-member-of (alexandria:shuffle (screamer::variable-enumerated-domain variable))))
408              (t (error "INTEGER-RANDOM-FORCE is currently only implemented for ~
409                         variables that have an enumerated domain."))))))
410   (value-of variable))
411
412 (export 'colorize)
413 (defun colorize (colors objects neighbours-fn)
414   (let* ((number-of-colors (length colors))
415          (object2color-var (make-hash-table))
416          (color-vars (mapcar #'(lambda (obj)
417                                  (setf (gethash obj object2color-var)
418                                        (an-integer-betweenv 1 number-of-colors)))
419                              objects))
420          (hash (make-hash-table :size (hash-table-size object2color-var))))
421     (dolist (obj objects)
422       (setf (gethash obj hash) nil))
423     (loop for obj in objects
424        for obj-color in color-vars
425        do (dolist (neighbour (funcall neighbours-fn obj))
426             (unless (member obj (gethash neighbour hash))
427               (let ((neighbour-color (gethash neighbour object2color-var)))
428                 (assert! (notv (=v obj-color neighbour-color)))
429                 (push obj (gethash neighbour hash))))))
430     (one-value (mapcar #'(lambda (color-index) (nth (1- color-index) colors))
431                        (solution color-vars (static-ordering #'integer-random-force)))
432                (error "no solution to colorize problem"))))
433
434 (in-package :geometry)
435
436 (defun nodes-connected-p (nodes node-neighbours &optional (test #'eql))
437   (let ((hash (make-hash-table :test test)))
438     (labels ((visited-p (node)
439                (gethash node hash))
440              (mark (node)
441                (setf (gethash node hash) t))
442              (traverse (stack)
443                (let ((current (pop stack)))
444                  (when current
445                    (mark current)
446                    (dolist (neighbour (funcall node-neighbours current))
447                      (unless (visited-p neighbour)
448                        (push neighbour stack)))
449                    (traverse stack)))))
450       (traverse (list (first nodes)))
451       (= (length nodes)
452          (hash-table-count hash)))))
453
454 (defun ascii-plot-points (objects &key key)
455   (fresh-line)
456   (let ((bbox (bounding-box objects :key key)))
457     (with-rectangle bbox
458       (loop for y from top below (+ top height)
459          do (loop for x from left below (+ left width)
460                if (member (list x y) objects :key key :test #'equal)
461                do (princ "x")
462                else do (princ "."))
463          do (terpri)))))
Note: See TracBrowser for help on using the browser.