-
Notifications
You must be signed in to change notification settings - Fork 19
/
UkkonenVisualization.elm
362 lines (290 loc) · 12 KB
/
UkkonenVisualization.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
module UkkonenVisualization (..) where
import Array exposing (..)
import Html exposing (..)
import Html.Attributes exposing (id, class, disabled)
import Html.Events exposing (onClick)
import Text
import Color exposing (..)
import Dict
import Graphics.Element exposing (..)
import Graphics.Input exposing (..)
import Graphics.Input.Field exposing (..)
import Json.Encode as Json
import List exposing (..)
import Markdown
import String exposing (..)
import Window
import UkkonenTree exposing (..)
import UkkonenAlgorithm exposing (..)
baseColor =
rgb 57 75 169
lightGrayColor =
rgb 68 68 68
port tree : Signal Json.Value
port tree =
Signal.map
(\( currentStep, steps, string ) ->
case Array.get currentStep steps of
Just state ->
treeJson
state.tree
state.activePoint
(String.slice
0
(currentStep + 1)
(String.fromList (Array.toList state.string))
)
Nothing ->
Json.null
)
(Signal.dropRepeats (Signal.map (\m -> ( m.currentStep, m.steps, m.string )) model))
type alias Model =
{ string : String
, steps : Array UkkonenState
, currentStep : Int
, inputField : Content
}
type Action
= NoOp
| InputFieldUpdate Content
| Build String
| Back
| Forward
initialModel =
{ string = ""
, steps = Array.empty
, currentStep = 0
, inputField = noContent
}
inputString : Signal.Mailbox Content
inputString =
Signal.mailbox noContent
inputButton : Signal.Mailbox Action
inputButton =
Signal.mailbox NoOp
inputFieldStyle : Style
inputFieldStyle =
let
textDefaultStyle = Text.defaultStyle
in
{ defaultStyle
| padding = uniformly -6
, outline = { color = lightGrayColor, width = uniformly 1, radius = 4 }
, style = { textDefaultStyle | height = Just 25, color = lightGrayColor }
}
inputField : Content -> Element
inputField =
field inputFieldStyle (Signal.message inputString.address) "input string..."
inputFieldUpdates : Signal Action
inputFieldUpdates =
Signal.map (\content -> InputFieldUpdate content) inputString.signal
visualizeButton : Element
visualizeButton =
Graphics.Input.button (Signal.message inputButton.address NoOp) "build suffix tree"
stringUpdates : Signal Action
stringUpdates =
Signal.map2
(\_ inputContent -> Build inputContent.string)
inputButton.signal
(Signal.sampleOn inputButton.signal inputString.signal)
leftButton : Bool -> Html
leftButton enabled =
Html.button [ onClick currentStepUpdates.address Back, disabled <| not enabled ] [ text "< prev" ]
rightButton : Bool -> Html
rightButton enabled =
Html.button [ onClick currentStepUpdates.address Forward, disabled <| not enabled ] [ text "next >" ]
currentStepUpdates : Signal.Mailbox Action
currentStepUpdates =
Signal.mailbox NoOp
main : Signal Html
main =
Signal.map view model
actions : Signal Action
actions =
Signal.mergeMany [ stringUpdates, currentStepUpdates.signal, inputFieldUpdates ]
model : Signal Model
model =
Signal.foldp update initialModel actions
update : Action -> Model -> Model
update action model =
case action of
InputFieldUpdate content ->
{ model | inputField = content }
Build string ->
if string == "" then
model
else
let
terminatedString = string ++ "$"
steps = Array.fromList (initialState :: UkkonenAlgorithm.steps terminatedString)
in
{ model | string = terminatedString, steps = steps, currentStep = 0, inputField = noContent }
Back ->
{ model | currentStep = max (model.currentStep - 1) 0 }
Forward ->
{ model | currentStep = min (model.currentStep + 1) ((Array.length model.steps) - 1) }
NoOp ->
model
introText : Html
introText =
Markdown.toHtml """
[Ukkonen's algorithm](https://en.wikipedia.org/wiki/Ukkonen's_algorithm) is a method of constructing the [suffix tree](https://en.wikipedia.org/wiki/Suffix_tree) of a string in linear time. Suffix trees are useful because they can efficiently answer many questions about a string, such as how many times a given substring occurs within the string. Enter an input string below and you'll be able to watch step-by-step as Ukkonen's algorithm builds a suffix tree.
I was inspired to build this visualization after reading [this great explanation](http://stackoverflow.com/a/9513423) of Ukkonen's algorithm. I'd recommend first reading that for an overview of how the algorithm works and then playing around with this visualization. Also quite helpful is the explanation given in [this video](https://www.youtube.com/watch?v=aPRqocoBsFQ).
"""
view : Model -> Html
view model =
let
leftButtonEnabled = model.currentStep > 0
rightButtonEnabled = model.currentStep < (Array.length model.steps) - 1
in
div
[ id "visualization" ]
[ div
[ id "heading" ]
[ h1 [] [ text "Visualization of Ukkonen's Algorithm" ]
, introText
, div
[ id "input-string" ]
[ inputField model.inputField |> width 400 |> fromElement
, span [ id "input-button-wrapper" ] [ visualizeButton |> width 150 |> fromElement ]
]
]
, div
[ id "steps-wrapper" ]
[ div
[ id "side-box" ]
[ h2 [] [ text <| "Step " ++ (Basics.toString (model.currentStep + 1)) ++ " of " ++ Basics.toString (Array.length model.steps) ]
, div
[ id "navigation" ]
[ span [ id "left-button-wrapper" ] [ leftButton leftButtonEnabled ]
, span [ id "right-button-wrapper" ] [ rightButton rightButtonEnabled ]
]
, case Array.get model.currentStep model.steps of
Just state ->
let
activeNodeString = Basics.toString state.activePoint.nodeId
activeEdgeString =
case state.activePoint.edge of
Just ( edge, steps ) ->
fromChar edge
Nothing ->
"none"
activeLengthString =
case state.activePoint.edge of
Just ( edge, steps ) ->
Basics.toString steps
Nothing ->
"0"
remainderString = Basics.toString (state.remainder - 1)
in
ul
[ id "algorithm-state" ]
[ li
[]
[ span [] [ text "active_node:" ]
, span [ id "var-active-node" ] [ text activeNodeString ]
]
, li
[]
[ span [] [ text "active_edge:" ]
, span [ id "var-active-edge" ] [ text activeEdgeString ]
]
, li
[]
[ span [] [ text "active_length:" ]
, span [ id "var-active-length" ] [ text activeLengthString ]
]
, li
[]
[ span [] [ text "remainder:" ]
, span [ id "var-remainder" ] [ text remainderString ]
]
]
Nothing ->
text ""
]
, div [ id "letter-blocks" ] (letterBlocks model.string model.currentStep model.steps)
]
]
{-| Generate the input string letter blocks
-}
letterBlocks : String -> Int -> Array UkkonenState -> List Html
letterBlocks string currentStep steps =
case Array.get currentStep steps of
Just state ->
List.indexedMap
(\i c ->
let
charsAdded = state.charsAdded
added =
if i < charsAdded then
[ class "added" ]
else
[]
remainder =
if i > charsAdded - state.remainder && i < charsAdded then
[ class "remainder" ]
else
[]
in
div
(List.append added remainder)
[ text (fromChar c) ]
)
(String.toList string)
Nothing ->
[]
{-| Prints out a JSON representation of the tree
-}
treeJson : UkkonenTree -> ActivePoint -> String -> Json.Value
treeJson tree activePoint string =
treeJson' 0 tree activePoint string
treeJson' : Int -> UkkonenTree -> ActivePoint -> String -> Json.Value
treeJson' rootId tree activePoint string =
let
root = getNode rootId tree
isActivePoint = (activePoint.nodeId == rootId)
in
Json.object
[ ( "id", Json.int rootId )
, ( "suffixLink"
, case root.suffixLink of
Just n ->
Json.int n
Nothing ->
Json.null
)
, ( "isActivePoint", Json.bool isActivePoint )
, ( "children"
, Json.object
(List.map
(\( c, edge ) ->
let
labelEnd =
case edge.labelEnd of
Definite l ->
l
EndOfString ->
String.length string
in
( fromChar c
, Json.object
[ ( "label", Json.string <| String.slice edge.labelStart labelEnd string )
, ( "pointingTo", treeJson' edge.pointingTo tree activePoint string )
, ( "edgeSteps"
, case activePoint.edge of
Just ( ac, edgeSteps ) ->
if (isActivePoint && c == ac) then
Json.int edgeSteps
else
Json.null
Nothing ->
Json.null
)
]
)
)
(Dict.toList root.edges)
)
)
]