Parse widgets v2 - globals
[clnl] / src / main / model.lisp
1 (in-package #:clnl-model)
2
3 (defvar *separator* "@#$#@#$#@")
4
5 (defstruct model
6  code
7  interface
8  info
9  turtle-shapes
10  version
11  preview-commands
12  system-dynamics
13  behavior-space
14  hub-net-client
15  link-shapes
16  model-settings
17  delta-tick)
18
19 (defun default-model ()
20  "DEFAULT-MODEL => MODEL
21
22 ARGUMENTS AND VALUES:
23
24   MODEL: an object representing the model
25
26 DESCRIPTION:
27
28   Returns the default startup model."
29  (make-model
30   :interface (list
31               (make-view :min-pxcor -5 :max-pxcor 5 :min-pycor -5 :max-pycor 5))))
32
33 (defun read-from-nlogo (str)
34  "READ-FROM-NLOGO STR => MODEL
35
36 ARGUMENTS AND VALUES:
37
38   STR: a readable stream
39   MODEL: an object representing the model
40
41 DESCRIPTION:
42
43   Takes a stream STR, reads in a nlogo file, parses it, and then
44   returns the model object."
45  (let
46   ((sections
47     (labels
48      ((read-sections (&optional section)
49        (let
50         ((line (read-line str nil)))
51         (when line
52          (if (string= *separator* line)
53           (cons section (read-sections))
54           (read-sections (append section (list line))))))))
55      (read-sections))))
56   (make-model
57    :code (nth 0 sections)
58    :interface (parse-interface (nth 1 sections))
59    :info (nth 2 sections)
60    :turtle-shapes (nth 3 sections)
61    :version (nth 4 sections)
62    :preview-commands (nth 5 sections)
63    :system-dynamics (nth 6 sections)
64    :behavior-space (nth 7 sections)
65    :hub-net-client (nth 8 sections)
66    :link-shapes (nth 9 sections)
67    :model-settings (nth 10 sections)
68    :delta-tick (nth 11 sections))))
69
70 ;;; INTERFACE PARSER
71
72 (defparameter *widget-parsers* nil)
73
74 (defmacro defwidget-definition (type &rest definitions)
75  (let
76   ((lines (gensym)))
77   `(progn
78     (defstruct ,type
79      ,@(remove nil
80         (mapcar
81          (lambda (def)
82           (when (find (car def) (list :int :double :inverted-boolean :boolean :choice :string :option)) (second def)))
83          definitions)))
84     (push
85      (list
86       (lambda (,lines)
87        (and
88         ,@(remove nil
89            (mapcar
90             (lambda (def n)
91              (let
92               ((line `(nth ,n ,lines)))
93               (case (car def)
94                (:specified `(string= ,(second def) ,line))
95                (:int `(parse-integer ,line :junk-allowed t))
96                (:double `(ignore-errors (coerce (read-from-string ,line) 'double-float)))
97                (:boolean `(or (string= "1" ,line) (string= "0" ,line)))
98                (:inverted-boolean `(or (string= "0" ,line) (string= "1" ,line)))
99                (:choice `(find ,line ',(mapcar #'car (third def)) :test #'string=)))))
100             definitions
101             (loop for i to (length definitions) collect i)))))
102       (lambda (,lines)
103        (,(read-from-string (format nil "make-~A" type))
104         ,@(apply #'append
105            (mapcar
106             (lambda (def n)
107              (let*
108               ((line `(nth ,n ,lines))
109                (val-getter
110                 (case (car def)
111                  (:int `(parse-integer ,line))
112                  (:double `(coerce (read-from-string ,line) 'double-float))
113                  (:boolean `(string= "1" ,line))
114                  (:inverted-boolean `(string= "0" ,line))
115                  (:choice `(cadr (find ,line ',(third def) :key #'car :test #'string=)))
116                  (:option `(when (string/= ,line ,(third def)) ,line))
117                  (:string line))))
118               (when val-getter (list (intern (symbol-name (cadr def)) :keyword) val-getter))))
119             definitions
120             (loop for i to (length definitions) collect i))))))
121      *widget-parsers*))))
122
123 (defwidget-definition view
124  (:specified "GRAPHICS-WINDOW")
125  (:int left)
126  (:int top)
127  (:int right)
128  (:int bottom)
129  (:reserved "-1")
130  (:reserved "-1")
131  (:double patch-size)
132  (:reserved)
133  (:int font-size)
134  (:reserved)
135  (:reserved)
136  (:reserved)
137  (:reserved)
138  (:boolean wrapping-allowed-in-x)
139  (:boolean wrapping-allowed-in-y)
140  (:reserved)
141  (:int min-pxcor)
142  (:int max-pxcor)
143  (:int min-pycor)
144  (:int max-pycor)
145  (:choice update-mode (("0" :continuous) ("1" :tick-based)))
146  (:dump update-mode)
147  (:boolean show-tick-counter)
148  (:string tick-counter-label)
149  (:double frame-rate 30))
150
151 (defwidget-definition slider
152  (:specified "SLIDER")
153  (:int left)
154  (:int top)
155  (:int right)
156  (:int bottom)
157  (:string display)
158  (:string varname)
159  (:string min)
160  (:string max)
161  (:double default)
162  (:string step)
163  (:reserved)
164  (:option units "NIL")
165  (:choice direction (("HORIZONTAL" :horizontal) ("VERTICAL" :vertical))))
166
167 (defwidget-definition switch
168  (:specified "SWITCH")
169  (:int left)
170  (:int top)
171  (:int right)
172  (:int bottom)
173  (:string display)
174  (:string varname)
175  (:inverted-boolean on)
176  (:reserved)
177  (:reserved))
178
179 (defun parse-interface (interface-as-strings)
180  (let
181   ((widgets-as-strings
182     (labels
183      ((separate-widgets-as-strings (lines &optional widget-as-strings)
184        (when lines
185         (if (string= "" (car lines))
186          (cons widget-as-strings (separate-widgets-as-strings (cdr lines)))
187          (separate-widgets-as-strings (cdr lines) (append widget-as-strings (list (car lines))))))))
188      (separate-widgets-as-strings interface-as-strings))))
189   (remove
190    nil
191    (mapcar
192     (lambda (widget-as-strings)
193      (let
194       ((parser (find-if (lambda (validator) (funcall validator widget-as-strings)) *widget-parsers* :key #'car)))
195       (when parser (funcall (cadr parser) widget-as-strings))))
196     widgets-as-strings))))
197
198 ;; INFORMATION ABOUT MODEL
199
200 (defun world-dimensions (model)
201  "WORLD-DIMENSIONS MODEL => DIMS
202
203   DIMS: (:xmin XMIN :xmax XMAX :ymin YMIN :ymax YMAX)
204
205 ARGUMENTS AND VALUES:
206
207   MODEL: A valid model containing a view
208   XMIN: An integer representing the minimum patch coord in X
209   XMAX: An integer representing the maximum patch coord in X
210   YMIN: An integer representing the minimum patch coord in Y
211   YMAX: An integer representing the maximum patch coord in Y
212
213 DESCRIPTION:
214
215   Returns the dimensions of MODEL.  MODEL must be a valid model
216   as parsed by CLNL, and have a valid view in it."
217  (let
218   ((view (find-if #'view-p (model-interface model))))
219   (list
220    :xmin (view-min-pxcor view)
221    :xmax (view-max-pxcor view)
222    :ymin (view-min-pycor view)
223    :ymax (view-max-pycor view))))
224
225 (defun globals (model)
226  "GLOBALS MODEL => GLOBALS
227
228   GLOBALS: GLOBAL*
229
230 ARGUMENTS AND VALUES:
231
232   MODEL: A valid model
233   GLOBAL: A symbol interned in clnl:*model-package*
234
235 DESCRIPTION:
236
237   Returns the globals that get declared in the model, from widgets or
238   from code.  They are interned in the package set for clnl, so
239   that they can later be used by functions in that package."
240  (mapcar
241   (lambda (pair)
242    (list
243     (intern (string-upcase (car pair)) clnl:*model-package*)
244     (cadr pair)))
245   (remove nil
246    (mapcar
247     (lambda (widget)
248      (typecase widget
249       (slider (list (slider-varname widget) (slider-default widget)))
250       (switch (list (switch-varname widget) (switch-on widget)))))
251     (model-interface model)))))