Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
M
Molstar
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Michal Malý
Molstar
Commits
9fba2d67
Commit
9fba2d67
authored
6 years ago
by
David Sehnal
Browse files
Options
Downloads
Patches
Plain Diff
mol-state: cleanup and fixes
parent
629f7c0c
Branches
Branches containing commit
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
src/mol-plugin/ui/state-tree.tsx
+1
-1
1 addition, 1 deletion
src/mol-plugin/ui/state-tree.tsx
src/mol-state/state.ts
+55
-32
55 additions, 32 deletions
src/mol-state/state.ts
with
56 additions
and
33 deletions
src/mol-plugin/ui/state-tree.tsx
+
1
−
1
View file @
9fba2d67
...
@@ -13,7 +13,7 @@ import { PluginCommands } from 'mol-plugin/command';
...
@@ -13,7 +13,7 @@ import { PluginCommands } from 'mol-plugin/command';
export
class
StateTree
extends
React
.
Component
<
{
plugin
:
PluginContext
,
state
:
State
},
{
}
>
{
export
class
StateTree
extends
React
.
Component
<
{
plugin
:
PluginContext
,
state
:
State
},
{
}
>
{
componentDidMount
()
{
componentDidMount
()
{
// TODO: move to constructor?
// TODO: move to constructor?
this
.
props
.
state
.
events
.
updat
ed
.
subscribe
(()
=>
this
.
forceUpdate
());
this
.
props
.
state
.
events
.
chang
ed
.
subscribe
(()
=>
this
.
forceUpdate
());
}
}
render
()
{
render
()
{
// const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
// const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
...
...
This diff is collapsed.
Click to expand it.
src/mol-state/state.ts
+
55
−
32
View file @
9fba2d67
...
@@ -29,6 +29,7 @@ class State {
...
@@ -29,6 +29,7 @@ class State {
readonly
events
=
{
readonly
events
=
{
object
:
{
object
:
{
cellState
:
this
.
ev
<
State
.
ObjectEvent
&
{
cell
:
StateObjectCell
}
>
(),
cellState
:
this
.
ev
<
State
.
ObjectEvent
&
{
cell
:
StateObjectCell
}
>
(),
cellCreated
:
this
.
ev
<
State
.
ObjectEvent
>
(),
updated
:
this
.
ev
<
State
.
ObjectEvent
&
{
obj
?:
StateObject
}
>
(),
updated
:
this
.
ev
<
State
.
ObjectEvent
&
{
obj
?:
StateObject
}
>
(),
replaced
:
this
.
ev
<
State
.
ObjectEvent
&
{
oldObj
?:
StateObject
,
newObj
?:
StateObject
}
>
(),
replaced
:
this
.
ev
<
State
.
ObjectEvent
&
{
oldObj
?:
StateObject
,
newObj
?:
StateObject
}
>
(),
...
@@ -36,7 +37,7 @@ class State {
...
@@ -36,7 +37,7 @@ class State {
removed
:
this
.
ev
<
State
.
ObjectEvent
&
{
obj
?:
StateObject
}
>
()
removed
:
this
.
ev
<
State
.
ObjectEvent
&
{
obj
?:
StateObject
}
>
()
},
},
warn
:
this
.
ev
<
string
>
(),
warn
:
this
.
ev
<
string
>
(),
updat
ed
:
this
.
ev
<
void
>
()
chang
ed
:
this
.
ev
<
void
>
()
};
};
readonly
behaviors
=
{
readonly
behaviors
=
{
...
@@ -84,7 +85,7 @@ class State {
...
@@ -84,7 +85,7 @@ class State {
return
StateSelection
.
select
(
selector
(
StateSelection
.
Generators
),
this
)
return
StateSelection
.
select
(
selector
(
StateSelection
.
Generators
),
this
)
}
}
/** I
s
no ref is specified, apply to root */
/** I
f
no ref is specified, apply to root */
apply
<
A
extends
StateAction
>
(
action
:
A
,
params
:
StateAction
.
Params
<
A
>
,
ref
:
Transform
.
Ref
=
Transform
.
RootRef
):
Task
<
void
>
{
apply
<
A
extends
StateAction
>
(
action
:
A
,
params
:
StateAction
.
Params
<
A
>
,
ref
:
Transform
.
Ref
=
Transform
.
RootRef
):
Task
<
void
>
{
return
Task
.
create
(
'
Apply Action
'
,
ctx
=>
{
return
Task
.
create
(
'
Apply Action
'
,
ctx
=>
{
const
cell
=
this
.
cells
.
get
(
ref
);
const
cell
=
this
.
cells
.
get
(
ref
);
...
@@ -96,9 +97,9 @@ class State {
...
@@ -96,9 +97,9 @@ class State {
}
}
update
(
tree
:
StateTree
|
StateTreeBuilder
):
Task
<
void
>
{
update
(
tree
:
StateTree
|
StateTreeBuilder
):
Task
<
void
>
{
// TODO: support cell state
const
_tree
=
StateTreeBuilder
.
is
(
tree
)
?
tree
.
getTree
()
:
tree
;
const
_tree
=
StateTreeBuilder
.
is
(
tree
)
?
tree
.
getTree
()
:
tree
;
return
Task
.
create
(
'
Update Tree
'
,
async
taskCtx
=>
{
return
Task
.
create
(
'
Update Tree
'
,
async
taskCtx
=>
{
let
updated
=
false
;
try
{
try
{
const
oldTree
=
this
.
_tree
;
const
oldTree
=
this
.
_tree
;
this
.
_tree
=
_tree
;
this
.
_tree
=
_tree
;
...
@@ -109,12 +110,13 @@ class State {
...
@@ -109,12 +110,13 @@ class State {
oldTree
,
oldTree
,
tree
:
_tree
,
tree
:
_tree
,
cells
:
this
.
cells
as
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
cells
:
this
.
cells
as
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
transformCache
:
this
.
transformCache
transformCache
:
this
.
transformCache
,
changed
:
false
};
};
// TODO: ha
v
e "cancelled" error? Or would this be handled automatically?
// TODO: ha
ndl
e "cancelled" error? Or would this be handled automatically?
await
update
(
ctx
);
updated
=
await
update
(
ctx
);
}
finally
{
}
finally
{
this
.
events
.
updat
ed
.
next
();
if
(
updated
)
this
.
events
.
chang
ed
.
next
();
}
}
});
});
}
}
...
@@ -164,11 +166,13 @@ interface UpdateContext {
...
@@ -164,11 +166,13 @@ interface UpdateContext {
oldTree
:
StateTree
,
oldTree
:
StateTree
,
tree
:
StateTree
,
tree
:
StateTree
,
cells
:
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
cells
:
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
transformCache
:
Map
<
Ref
,
unknown
>
transformCache
:
Map
<
Ref
,
unknown
>
,
changed
:
boolean
}
}
async
function
update
(
ctx
:
UpdateContext
)
{
async
function
update
(
ctx
:
UpdateContext
)
{
const
roots
=
findUpdateRoots
(
ctx
.
cells
,
ctx
.
tree
);
// 1: find all nodes that will definitely be deleted.
// this is done in "post order", meaning that leaves will be deleted first.
const
deletes
=
findDeletes
(
ctx
);
const
deletes
=
findDeletes
(
ctx
);
for
(
const
d
of
deletes
)
{
for
(
const
d
of
deletes
)
{
const
obj
=
ctx
.
cells
.
has
(
d
)
?
ctx
.
cells
.
get
(
d
)
!
.
obj
:
void
0
;
const
obj
=
ctx
.
cells
.
has
(
d
)
?
ctx
.
cells
.
get
(
d
)
!
.
obj
:
void
0
;
...
@@ -178,23 +182,33 @@ async function update(ctx: UpdateContext) {
...
@@ -178,23 +182,33 @@ async function update(ctx: UpdateContext) {
// TODO: handle current object change
// TODO: handle current object change
}
}
// 2: Find roots where transform version changed or where nodes will be added.
const
roots
=
findUpdateRoots
(
ctx
.
cells
,
ctx
.
tree
);
// 3: Init empty cells where not present
// this is done in "pre order", meaning that "parents" will be created 1st.
initCells
(
ctx
,
roots
);
initCells
(
ctx
,
roots
);
// 4: Set status of cells that will be updated to 'pending'.
initCellStatus
(
ctx
,
roots
);
initCellStatus
(
ctx
,
roots
);
// 6: Sequentially update all the subtrees.
for
(
const
root
of
roots
)
{
for
(
const
root
of
roots
)
{
await
updateSubtree
(
ctx
,
root
);
await
updateSubtree
(
ctx
,
root
);
}
}
return
deletes
.
length
>
0
||
roots
.
length
>
0
||
ctx
.
changed
;
}
}
function
findUpdateRoots
(
cells
:
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
tree
:
StateTree
)
{
function
findUpdateRoots
(
cells
:
Map
<
Transform
.
Ref
,
StateObjectCell
>
,
tree
:
StateTree
)
{
const
findState
=
{
roots
:
[]
as
Ref
[],
cells
};
const
findState
=
{
roots
:
[]
as
Ref
[],
cells
};
StateTree
.
doPreOrder
(
tree
,
tree
.
root
,
findState
,
_
findUpdateRoots
);
StateTree
.
doPreOrder
(
tree
,
tree
.
root
,
findState
,
findUpdateRoots
Visitor
);
return
findState
.
roots
;
return
findState
.
roots
;
}
}
function
_
findUpdateRoots
(
n
:
Transform
,
_
:
any
,
s
:
{
roots
:
Ref
[],
cells
:
Map
<
Ref
,
StateObjectCell
>
})
{
function
findUpdateRoots
Visitor
(
n
:
Transform
,
_
:
any
,
s
:
{
roots
:
Ref
[],
cells
:
Map
<
Ref
,
StateObjectCell
>
})
{
const
cell
=
s
.
cells
.
get
(
n
.
ref
);
const
cell
=
s
.
cells
.
get
(
n
.
ref
);
if
(
!
cell
||
cell
.
version
!==
n
.
version
)
{
if
(
!
cell
||
cell
.
version
!==
n
.
version
||
cell
.
status
===
'
error
'
)
{
s
.
roots
.
push
(
n
.
ref
);
s
.
roots
.
push
(
n
.
ref
);
return
false
;
return
false
;
}
}
...
@@ -219,19 +233,18 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
...
@@ -219,19 +233,18 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
if
(
changed
)
ctx
.
parent
.
events
.
object
.
cellState
.
next
({
state
:
ctx
.
parent
,
ref
,
cell
});
if
(
changed
)
ctx
.
parent
.
events
.
object
.
cellState
.
next
({
state
:
ctx
.
parent
,
ref
,
cell
});
}
}
function
_
initCellStatusVisitor
(
t
:
Transform
,
_
:
any
,
ctx
:
UpdateContext
)
{
function
initCellStatusVisitor
(
t
:
Transform
,
_
:
any
,
ctx
:
UpdateContext
)
{
ctx
.
cells
.
get
(
t
.
ref
)
!
.
transform
=
t
;
ctx
.
cells
.
get
(
t
.
ref
)
!
.
transform
=
t
;
setCellStatus
(
ctx
,
t
.
ref
,
'
pending
'
);
setCellStatus
(
ctx
,
t
.
ref
,
'
pending
'
);
}
}
/** Return "resolve set" */
function
initCellStatus
(
ctx
:
UpdateContext
,
roots
:
Ref
[])
{
function
initCellStatus
(
ctx
:
UpdateContext
,
roots
:
Ref
[])
{
for
(
const
root
of
roots
)
{
for
(
const
root
of
roots
)
{
StateTree
.
doPreOrder
(
ctx
.
tree
,
ctx
.
tree
.
nodes
.
get
(
root
),
ctx
,
_
initCellStatusVisitor
);
StateTree
.
doPreOrder
(
ctx
.
tree
,
ctx
.
tree
.
nodes
.
get
(
root
),
ctx
,
initCellStatusVisitor
);
}
}
}
}
function
_
initCellsVisitor
(
transform
:
Transform
,
_
:
any
,
ctx
:
UpdateContext
)
{
function
initCellsVisitor
(
transform
:
Transform
,
_
:
any
,
ctx
:
UpdateContext
)
{
if
(
ctx
.
cells
.
has
(
transform
.
ref
))
return
;
if
(
ctx
.
cells
.
has
(
transform
.
ref
))
return
;
const
obj
:
StateObjectCell
=
{
const
obj
:
StateObjectCell
=
{
...
@@ -242,38 +255,49 @@ function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) {
...
@@ -242,38 +255,49 @@ function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) {
errorText
:
void
0
errorText
:
void
0
};
};
ctx
.
cells
.
set
(
transform
.
ref
,
obj
);
ctx
.
cells
.
set
(
transform
.
ref
,
obj
);
ctx
.
parent
.
events
.
object
.
cellCreated
.
next
({
state
:
ctx
.
parent
,
ref
:
transform
.
ref
});
// TODO: created event???
}
}
function
initCells
(
ctx
:
UpdateContext
,
roots
:
Ref
[])
{
function
initCells
(
ctx
:
UpdateContext
,
roots
:
Ref
[])
{
for
(
const
root
of
roots
)
{
for
(
const
root
of
roots
)
{
StateTree
.
doPreOrder
(
ctx
.
tree
,
ctx
.
tree
.
nodes
.
get
(
root
),
ctx
,
_
initCellsVisitor
);
StateTree
.
doPreOrder
(
ctx
.
tree
,
ctx
.
tree
.
nodes
.
get
(
root
),
ctx
,
initCellsVisitor
);
}
}
}
}
function
doError
(
ctx
:
UpdateContext
,
ref
:
Ref
,
errorText
:
string
)
{
/** Set status and error text of the cell. Remove all existing objects in the subtree. */
setCellStatus
(
ctx
,
ref
,
'
error
'
,
errorText
);
function
doError
(
ctx
:
UpdateContext
,
ref
:
Ref
,
errorText
:
string
|
undefined
)
{
const
wrap
=
ctx
.
cells
.
get
(
ref
)
!
;
if
(
errorText
)
setCellStatus
(
ctx
,
ref
,
'
error
'
,
errorText
);
if
(
wrap
.
obj
)
{
ctx
.
parent
.
events
.
object
.
removed
.
next
({
state
:
ctx
.
parent
,
ref
});
const
cell
=
ctx
.
cells
.
get
(
ref
)
!
;
if
(
cell
.
obj
)
{
const
obj
=
cell
.
obj
;
cell
.
obj
=
void
0
;
ctx
.
parent
.
events
.
object
.
removed
.
next
({
state
:
ctx
.
parent
,
ref
,
obj
});
ctx
.
transformCache
.
delete
(
ref
);
ctx
.
transformCache
.
delete
(
ref
);
wrap
.
obj
=
void
0
;
}
}
// remove the objects in the child nodes if they exist
const
children
=
ctx
.
tree
.
children
.
get
(
ref
).
values
();
const
children
=
ctx
.
tree
.
children
.
get
(
ref
).
values
();
while
(
true
)
{
while
(
true
)
{
const
next
=
children
.
next
();
const
next
=
children
.
next
();
if
(
next
.
done
)
return
;
if
(
next
.
done
)
return
;
doError
(
ctx
,
next
.
value
,
'
Parent node contains error.
'
);
doError
(
ctx
,
next
.
value
,
void
0
);
}
}
}
}
type
UpdateNodeResult
=
|
{
action
:
'
created
'
,
obj
:
StateObject
}
|
{
action
:
'
updated
'
,
obj
:
StateObject
}
|
{
action
:
'
replaced
'
,
oldObj
?:
StateObject
,
newObj
:
StateObject
}
|
{
action
:
'
none
'
}
async
function
updateSubtree
(
ctx
:
UpdateContext
,
root
:
Ref
)
{
async
function
updateSubtree
(
ctx
:
UpdateContext
,
root
:
Ref
)
{
setCellStatus
(
ctx
,
root
,
'
processing
'
);
setCellStatus
(
ctx
,
root
,
'
processing
'
);
try
{
try
{
const
update
=
await
updateNode
(
ctx
,
root
);
const
update
=
await
updateNode
(
ctx
,
root
);
if
(
update
.
action
!==
'
none
'
)
ctx
.
changed
=
true
;
setCellStatus
(
ctx
,
root
,
'
ok
'
);
setCellStatus
(
ctx
,
root
,
'
ok
'
);
if
(
update
.
action
===
'
created
'
)
{
if
(
update
.
action
===
'
created
'
)
{
ctx
.
parent
.
events
.
object
.
created
.
next
({
state
:
ctx
.
parent
,
ref
:
root
,
obj
:
update
.
obj
!
});
ctx
.
parent
.
events
.
object
.
created
.
next
({
state
:
ctx
.
parent
,
ref
:
root
,
obj
:
update
.
obj
!
});
...
@@ -283,6 +307,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
...
@@ -283,6 +307,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
ctx
.
parent
.
events
.
object
.
replaced
.
next
({
state
:
ctx
.
parent
,
ref
:
root
,
oldObj
:
update
.
oldObj
,
newObj
:
update
.
newObj
});
ctx
.
parent
.
events
.
object
.
replaced
.
next
({
state
:
ctx
.
parent
,
ref
:
root
,
oldObj
:
update
.
oldObj
,
newObj
:
update
.
newObj
});
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
ctx
.
changed
=
true
;
doError
(
ctx
,
root
,
''
+
e
);
doError
(
ctx
,
root
,
''
+
e
);
return
;
return
;
}
}
...
@@ -295,7 +320,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
...
@@ -295,7 +320,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
}
}
}
}
async
function
updateNode
(
ctx
:
UpdateContext
,
currentRef
:
Ref
)
{
async
function
updateNode
(
ctx
:
UpdateContext
,
currentRef
:
Ref
)
:
Promise
<
UpdateNodeResult
>
{
const
{
oldTree
,
tree
}
=
ctx
;
const
{
oldTree
,
tree
}
=
ctx
;
const
transform
=
tree
.
nodes
.
get
(
currentRef
);
const
transform
=
tree
.
nodes
.
get
(
currentRef
);
const
parentCell
=
StateSelection
.
findAncestorOfType
(
tree
,
ctx
.
cells
,
currentRef
,
transform
.
transformer
.
definition
.
from
);
const
parentCell
=
StateSelection
.
findAncestorOfType
(
tree
,
ctx
.
cells
,
currentRef
,
transform
.
transformer
.
definition
.
from
);
...
@@ -308,9 +333,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) {
...
@@ -308,9 +333,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) {
const
current
=
ctx
.
cells
.
get
(
currentRef
)
!
;
const
current
=
ctx
.
cells
.
get
(
currentRef
)
!
;
current
.
sourceRef
=
parentCell
.
transform
.
ref
;
current
.
sourceRef
=
parentCell
.
transform
.
ref
;
// console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
if
(
!
oldTree
.
nodes
.
has
(
currentRef
))
{
if
(
!
oldTree
.
nodes
.
has
(
currentRef
))
{
// console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
const
obj
=
await
createObject
(
ctx
,
currentRef
,
transform
.
transformer
,
parent
,
transform
.
params
);
const
obj
=
await
createObject
(
ctx
,
currentRef
,
transform
.
transformer
,
parent
,
transform
.
params
);
current
.
obj
=
obj
;
current
.
obj
=
obj
;
current
.
version
=
transform
.
version
;
current
.
version
=
transform
.
version
;
...
@@ -333,7 +356,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) {
...
@@ -333,7 +356,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) {
}
}
case
Transformer
.
UpdateResult
.
Updated
:
case
Transformer
.
UpdateResult
.
Updated
:
current
.
version
=
transform
.
version
;
current
.
version
=
transform
.
version
;
return
{
action
:
'
updated
'
,
obj
:
current
.
obj
};
return
{
action
:
'
updated
'
,
obj
:
current
.
obj
!
};
default
:
default
:
return
{
action
:
'
none
'
};
return
{
action
:
'
none
'
};
}
}
...
@@ -346,7 +369,7 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
...
@@ -346,7 +369,7 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
}
}
function
createObject
(
ctx
:
UpdateContext
,
ref
:
Ref
,
transformer
:
Transformer
,
a
:
StateObject
,
params
:
any
)
{
function
createObject
(
ctx
:
UpdateContext
,
ref
:
Ref
,
transformer
:
Transformer
,
a
:
StateObject
,
params
:
any
)
{
const
cache
=
{}
;
const
cache
=
Object
.
create
(
null
)
;
ctx
.
transformCache
.
set
(
ref
,
cache
);
ctx
.
transformCache
.
set
(
ref
,
cache
);
return
runTask
(
transformer
.
definition
.
apply
({
a
,
params
,
cache
},
ctx
.
parent
.
globalContext
),
ctx
.
taskCtx
);
return
runTask
(
transformer
.
definition
.
apply
({
a
,
params
,
cache
},
ctx
.
parent
.
globalContext
),
ctx
.
taskCtx
);
}
}
...
@@ -357,7 +380,7 @@ async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transform
...
@@ -357,7 +380,7 @@ async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transform
}
}
let
cache
=
ctx
.
transformCache
.
get
(
ref
);
let
cache
=
ctx
.
transformCache
.
get
(
ref
);
if
(
!
cache
)
{
if
(
!
cache
)
{
cache
=
{}
;
cache
=
Object
.
create
(
null
)
;
ctx
.
transformCache
.
set
(
ref
,
cache
);
ctx
.
transformCache
.
set
(
ref
,
cache
);
}
}
return
runTask
(
transformer
.
definition
.
update
({
a
,
oldParams
,
b
,
newParams
,
cache
},
ctx
.
parent
.
globalContext
),
ctx
.
taskCtx
);
return
runTask
(
transformer
.
definition
.
update
({
a
,
oldParams
,
b
,
newParams
,
cache
},
ctx
.
parent
.
globalContext
),
ctx
.
taskCtx
);
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment