Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
imagej-elphel
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
3
Issues
3
List
Board
Labels
Milestones
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Elphel
imagej-elphel
Commits
d35ccbb6
Commit
d35ccbb6
authored
Oct 21, 2022
by
Andrey Filippov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
completed methods for multithreaded operation
parent
1aa3cc9e
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
219 additions
and
26 deletions
+219
-26
LwocLeaf.java
...n/java/com/elphel/imagej/tileprocessor/lwoc/LwocLeaf.java
+11
-5
LwocOctree.java
...java/com/elphel/imagej/tileprocessor/lwoc/LwocOctree.java
+208
-21
No files found.
src/main/java/com/elphel/imagej/tileprocessor/lwoc/LwocLeaf.java
View file @
d35ccbb6
...
@@ -36,17 +36,23 @@ public class LwocLeaf {
...
@@ -36,17 +36,23 @@ public class LwocLeaf {
scenes
=
new
ArrayList
<
LwocScene
>();
// cameras located in this node
scenes
=
new
ArrayList
<
LwocScene
>();
// cameras located in this node
}
}
public
void
addScene
(
LwocScene
scene
)
{
public
void
addScene
(
LwocScene
scene
,
boolean
check_existed
)
{
if
(!
check_existed
||
!
scenes
.
contains
(
scene
))
{
scenes
.
add
(
scene
);
scenes
.
add
(
scene
);
}
}
}
public
void
addMeshCenter
(
LwocMesh
mesh
)
{
public
void
addMeshCenter
(
LwocMesh
mesh
,
boolean
check_existed
)
{
if
(!
check_existed
||
!
mesh_centers
.
contains
(
mesh
))
{
mesh_centers
.
add
(
mesh
);
mesh_centers
.
add
(
mesh
);
}
}
}
public
void
addMesh
(
LwocMesh
mesh
,
public
void
addMesh
(
LwocMesh
mesh
,
boolean
check_existed
)
{
boolean
check_existed
)
{
if
(!
check_existed
||
meshes
.
contains
(
mesh
))
{
if
(!
check_existed
||
!
meshes
.
contains
(
mesh
))
{
meshes
.
add
(
mesh
);
meshes
.
add
(
mesh
);
}
}
}
}
...
...
src/main/java/com/elphel/imagej/tileprocessor/lwoc/LwocOctree.java
View file @
d35ccbb6
...
@@ -25,6 +25,7 @@
...
@@ -25,6 +25,7 @@
package
com
.
elphel
.
imagej
.
tileprocessor
.
lwoc
;
package
com
.
elphel
.
imagej
.
tileprocessor
.
lwoc
;
import
java.util.ArrayList
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
com.elphel.imagej.common.MultiThreading
;
import
com.elphel.imagej.common.MultiThreading
;
public
class
LwocOctree
{
public
class
LwocOctree
{
...
@@ -98,7 +99,6 @@ public class LwocOctree {
...
@@ -98,7 +99,6 @@ public class LwocOctree {
* @return if world needs growing.
* @return if world needs growing.
*/
*/
public
static
boolean
pendingGrow
()
{
public
static
boolean
pendingGrow
()
{
// return !pendingScenes.isEmpty() || !pendingMeshes.isEmpty();
return
(
numPendingScenes
.
get
()
+
numPendingMeshes
.
get
())
>
0
;
return
(
numPendingScenes
.
get
()
+
numPendingMeshes
.
get
())
>
0
;
}
}
...
@@ -154,24 +154,32 @@ public class LwocOctree {
...
@@ -154,24 +154,32 @@ public class LwocOctree {
* @param scene Scene to add.
* @param scene Scene to add.
* @param pendingScenes per thread list of scenes to be combined after merging threads
* @param pendingScenes per thread list of scenes to be combined after merging threads
* @param pendingLeafNodes per thread list of nodes to be combined after merging threads
* @param pendingLeafNodes per thread list of nodes to be combined after merging threads
* @param check_existed - do not add duplicate scenes and nodes
* @return An octree node where scene is added or null if the world needs growing
* @return An octree node where scene is added or null if the world needs growing
*/
*/
public
static
LwocOctree
addScene
(
public
static
LwocOctree
addScene
(
LwocScene
scene
,
LwocScene
scene
,
ArrayList
<
LwocScene
>
pendingScenes
,
ArrayList
<
LwocScene
>
pendingScenes
,
ArrayList
<
LwocOctree
>
pendingLeafNodes
ArrayList
<
LwocOctree
>
pendingLeafNodes
,
boolean
check_existed
)
{
// returns null and adds to pendingScenes if needs growing
)
{
// returns null and adds to pendingScenes if needs growing
LwocOctree
node
=
getLeafNode
(
scene
.
getCameraXYZ
());
LwocOctree
node
=
getLeafNode
(
scene
.
getCameraXYZ
());
if
(
node
==
null
)
{
if
(
node
==
null
)
{
if
(
pendingScenes
!=
null
)
{
if
(
pendingScenes
!=
null
)
{
if
(!
check_existed
||
!
pendingScenes
.
contains
(
scene
))
{
pendingScenes
.
add
(
scene
);
pendingScenes
.
add
(
scene
);
numPendingScenes
.
getAndIncrement
();
}
}
}
}
else
{
}
else
{
node
.
leaf
.
addScene
(
scene
);
node
.
leaf
.
addScene
(
scene
,
check_existed
);
// Check if leaf node requires splitting
// Check if leaf node requires splitting
if
(
node
.
leaf
.
getScenes
().
size
()
>
MAX_CAMERAS
)
{
if
(
node
.
leaf
.
getScenes
().
size
()
>
MAX_CAMERAS
)
{
if
(
node
.
hsize
>
MIN_HSIZE
)
{
if
(
node
.
hsize
>
MIN_HSIZE
)
{
if
(!
check_existed
||
!
pendingLeafNodes
.
contains
(
node
))
{
pendingLeafNodes
.
add
(
node
);
pendingLeafNodes
.
add
(
node
);
numPendingLeafNodes
.
getAndIncrement
();
}
}
}
}
}
}
}
...
@@ -182,10 +190,12 @@ public class LwocOctree {
...
@@ -182,10 +190,12 @@ public class LwocOctree {
* Tend to pending scenes list. Not thread-safe, should be run in a single-thread mode.
* Tend to pending scenes list. Not thread-safe, should be run in a single-thread mode.
* @param pendingScenes combined list of scenes to be added growing world (merged from per-thread ones)
* @param pendingScenes combined list of scenes to be added growing world (merged from per-thread ones)
* @param pendingLeafNodes a list of nodes that will need splitting (here in single-thread mode - single one)
* @param pendingLeafNodes a list of nodes that will need splitting (here in single-thread mode - single one)
* @param check_existed - do not add duplicate scenes and nodes
*/
*/
public
static
void
tendPendingScenes
(
public
static
void
tendPendingScenes
(
ArrayList
<
LwocScene
>
pendingScenes
,
ArrayList
<
LwocScene
>
pendingScenes
,
ArrayList
<
LwocOctree
>
pendingLeafNodes
ArrayList
<
LwocOctree
>
pendingLeafNodes
,
boolean
check_existed
)
{
)
{
if
(
pendingScenes
.
isEmpty
())
{
if
(
pendingScenes
.
isEmpty
())
{
return
;
// nothing to do
return
;
// nothing to do
...
@@ -195,7 +205,8 @@ public class LwocOctree {
...
@@ -195,7 +205,8 @@ public class LwocOctree {
LwocOctree
node
=
addScene
(
// should not be null
LwocOctree
node
=
addScene
(
// should not be null
scene
,
scene
,
pendingScenes
,
pendingScenes
,
pendingLeafNodes
);
pendingLeafNodes
,
check_existed
);
assert
node
!=
null
:
"addScene() should not fail after growing world"
;
assert
node
!=
null
:
"addScene() should not fail after growing world"
;
}
}
}
}
...
@@ -203,11 +214,12 @@ public class LwocOctree {
...
@@ -203,11 +214,12 @@ public class LwocOctree {
/**
/**
* Tend to pending meshes list. Not thread-safe, should be run in a single-thread mode.
* Tend to pending meshes list. Not thread-safe, should be run in a single-thread mode.
* Only adds mesh centers and grows the world if needed.
* @param check_existed do not add duplicate meshes
* @param check_existed do not add duplicate meshes
* @param pendingMeshes combined list of meshes to be added (merged from per-thread ones). Will only be read.
* @param pendingMeshes combined list of meshes to be added (merged from per-thread ones). Will only be read.
* @param pendingLeafNodes a list of nodes that will need splitting (here in single-thread mode - single one). May grow.
* @param pendingLeafNodes a list of nodes that will need splitting (here in single-thread mode - single one). May grow.
*/
*/
public
static
void
tendPendingMeshCenters
(
public
static
void
tendPendingMeshCenters
Only
(
boolean
check_existed
,
boolean
check_existed
,
ArrayList
<
LwocMesh
>
pendingMeshes
,
// will read
ArrayList
<
LwocMesh
>
pendingMeshes
,
// will read
ArrayList
<
LwocOctree
>
pendingLeafNodes
// may add to
ArrayList
<
LwocOctree
>
pendingLeafNodes
// may add to
...
@@ -232,6 +244,26 @@ public class LwocOctree {
...
@@ -232,6 +244,26 @@ public class LwocOctree {
null
,
// pendingMeshes,
null
,
// pendingMeshes,
pendingLeafNodes
);
pendingLeafNodes
);
assert
ok
:
"addMeshCenter() should not fail after growing world"
;
assert
ok
:
"addMeshCenter() should not fail after growing world"
;
}
}
}
/**
* Tend to pending meshes list after thge world is grown and related mesh centers are added.
* Only adds mesh themselves that do not trigger node splits.
* Should be run after tendPendingMeshCentersOnly().
* @param check_existed do not add duplicate meshes
* @param pendingMeshes combined list of meshes to be added (merged from per-thread ones).
* Will only be read.
*/
public
static
void
tendPendingMeshes
(
boolean
check_existed
,
ArrayList
<
LwocMesh
>
pendingMeshes
// will read
)
{
if
(
pendingMeshes
.
isEmpty
())
{
return
;
// nothing to do
}
else
{
for
(
LwocMesh
mesh:
pendingMeshes
)
{
// int num_added =
// int num_added =
lwoc_root
.
addMesh
(
lwoc_root
.
addMesh
(
mesh
,
mesh
,
...
@@ -240,13 +272,31 @@ public class LwocOctree {
...
@@ -240,13 +272,31 @@ public class LwocOctree {
}
}
}
}
/**
* Recursively (if needed) splits octree nodes to reduce number of scenes/cameras
* and mesh centers below specified thresholds (limited by the minimal node size)
* @param pendingLeafNodesIn array list of nodes to be split. Will be filtered to
* remove any duplicates and non-leaf nodes
* @param min_hsize Minimal half-size of the node (in meters). Smaller nodes will not be split
* @param max_mesh_centers Maximal number of mesh centers in a node. Larger number
* triggers node split.
* @param max_cameras Maximasl number of scenes (camera positions) in a node. Larger
* number triggers node split.
* @param check_existed
*/
public
static
void
tendPendingLeafNodes
(
public
static
void
tendPendingLeafNodes
(
ArrayList
<
LwocOctree
>
pendingLeafNodesIn
)
{
ArrayList
<
LwocOctree
>
pendingLeafNodesIn
,
final
double
min_hsize
,
final
int
max_mesh_centers
,
final
int
max_cameras
,
// scenes
final
boolean
check_existed
)
{
// remove any possible duplicates
// remove any possible duplicates
final
ArrayList
<
LwocOctree
>
pendingLeafNodes
=
new
ArrayList
<
LwocOctree
>();
final
ArrayList
<
LwocOctree
>
pendingLeafNodes
=
new
ArrayList
<
LwocOctree
>();
for
(
LwocOctree
node:
pendingLeafNodesIn
)
{
for
(
LwocOctree
node:
pendingLeafNodesIn
)
{
if
(!
pendingLeafNodes
.
contains
(
node
)
)
{
if
(!
pendingLeafNodes
.
contains
(
node
)
&&
node
.
isLeaf
())
{
// filter already split nodes
pendingLeafNodes
.
add
(
node
);
pendingLeafNodes
.
add
(
node
);
numPendingLeafNodes
.
getAndIncrement
();
}
}
}
}
// run splitting multithreaded
// run splitting multithreaded
...
@@ -259,6 +309,11 @@ public class LwocOctree {
...
@@ -259,6 +309,11 @@ public class LwocOctree {
for
(
int
indx_node
=
ai
.
getAndIncrement
();
indx_node
<
pendingLeafNodes
.
size
();
indx_node
=
ai
.
getAndIncrement
())
{
for
(
int
indx_node
=
ai
.
getAndIncrement
();
indx_node
<
pendingLeafNodes
.
size
();
indx_node
=
ai
.
getAndIncrement
())
{
LwocOctree
node
=
pendingLeafNodes
.
get
(
indx_node
);
LwocOctree
node
=
pendingLeafNodes
.
get
(
indx_node
);
// Split recursively until satisfied conditions
// Split recursively until satisfied conditions
node
.
splitNode
(
min_hsize
,
// double min_hsize,
max_mesh_centers
,
// int max_mesh_centers,
max_cameras
,
// int max_cameras,
check_existed
);
// boolean check_existed)
}
// end of tile
}
// end of tile
}
}
};
};
...
@@ -271,7 +326,8 @@ public class LwocOctree {
...
@@ -271,7 +326,8 @@ public class LwocOctree {
public
int
splitNode
(
public
int
splitNode
(
double
min_hsize
,
double
min_hsize
,
int
max_mesh_centers
,
int
max_mesh_centers
,
int
max_cameras
int
max_cameras
,
boolean
check_existed
)
{
)
{
if
((
hsize
<=
min_hsize
)
||
(
leaf
==
null
))
{
if
((
hsize
<=
min_hsize
)
||
(
leaf
==
null
))
{
return
0
;
return
0
;
...
@@ -298,17 +354,53 @@ public class LwocOctree {
...
@@ -298,17 +354,53 @@ public class LwocOctree {
}
}
ArrayList
<
LwocMesh
>
meshes
=
leaf
.
getMeshes
();
ArrayList
<
LwocMesh
>
meshes
=
leaf
.
getMeshes
();
// distribute meshes between 8 children
// distribute meshes between 8 children
for
(
LwocMesh
mesh:
meshes
)
{
// may go to any number >=1 of the children
double
[]
mesh_center
=
mesh
.
getCenter
();
double
[]
mesh_dims
=
mesh
.
getDims
();
for
(
LwocOctree
node:
children
)
{
if
(
node
.
intersects
(
mesh_center
,
mesh_dims
))
{
node
.
leaf
.
addMesh
(
mesh
,
check_existed
);
}
}
}
ArrayList
<
LwocMesh
>
mesh_centers
=
leaf
.
getMeshCenters
();
ArrayList
<
LwocMesh
>
mesh_centers
=
leaf
.
getMeshCenters
();
// distribute mesh_centers between 8 children
// distribute mesh_centers between 8 children
for
(
LwocMesh
mesh:
mesh_centers
)
{
// goes to a single child only
double
[]
mesh_center
=
mesh
.
getCenter
();
for
(
LwocOctree
node:
children
)
{
if
(
node
.
contains
(
mesh_center
))
{
node
.
leaf
.
addMeshCenter
(
mesh
,
check_existed
);
break
;
// single point - goes to one child only
}
}
}
ArrayList
<
LwocScene
>
scenes
=
leaf
.
getScenes
();
ArrayList
<
LwocScene
>
scenes
=
leaf
.
getScenes
();
// distribute scnes (cameras) between 8 children
// distribute scenes (cameras) between 8 children
for
(
LwocScene
scene:
scenes
)
{
// goes to a single child only
double
[]
camera_xyz
=
scene
.
getCameraXYZ
();
for
(
LwocOctree
node:
children
)
{
if
(
node
.
contains
(
camera_xyz
))
{
node
.
leaf
.
addScene
(
scene
,
check_existed
);
break
;
// single point - goes to one child only
}
}
}
// delete old leaf (if it is in a list) and its lists - no need
// delete old leaf (if it is in a list) and its lists - no need
leaf
=
null
;
leaf
=
null
;
for
(
int
nchild
=
0
;
nchild
<
children
.
length
;
nchild
++)
{
for
(
int
nchild
=
0
;
nchild
<
children
.
length
;
nchild
++)
{
num_added
+=
children
[
nchild
].
splitNode
(
num_added
+=
children
[
nchild
].
splitNode
(
min_hsize
,
min_hsize
,
max_mesh_centers
,
max_mesh_centers
,
max_cameras
);
max_cameras
,
check_existed
);
}
}
return
num_added
;
return
num_added
;
}
}
...
@@ -445,14 +537,20 @@ public class LwocOctree {
...
@@ -445,14 +537,20 @@ public class LwocOctree {
corner_xyz
[
dm
]
+=
dims
[
dm
];
corner_xyz
[
dm
]
+=
dims
[
dm
];
if
(!
lwoc_root
.
contains
(
corner_xyz
)){
if
(!
lwoc_root
.
contains
(
corner_xyz
)){
if
(
pendingMeshes
!=
null
)
{
if
(
pendingMeshes
!=
null
)
{
if
(!
check_existed
||
!
pendingMeshes
.
contains
(
mesh
))
{
pendingMeshes
.
add
(
mesh
);
pendingMeshes
.
add
(
mesh
);
numPendingMeshes
.
getAndIncrement
();
}
}
}
return
false
;
return
false
;
}
}
corner_xyz
[
dm
]
-=
2
*
dims
[
dm
];
corner_xyz
[
dm
]
-=
2
*
dims
[
dm
];
if
(!
lwoc_root
.
contains
(
corner_xyz
)){
if
(!
lwoc_root
.
contains
(
corner_xyz
)){
if
(
pendingMeshes
!=
null
)
{
if
(
pendingMeshes
!=
null
)
{
if
(!
check_existed
||
!
pendingMeshes
.
contains
(
mesh
))
{
pendingMeshes
.
add
(
mesh
);
pendingMeshes
.
add
(
mesh
);
numPendingMeshes
.
getAndIncrement
();
}
}
}
return
false
;
return
false
;
}
}
...
@@ -460,18 +558,22 @@ public class LwocOctree {
...
@@ -460,18 +558,22 @@ public class LwocOctree {
}
}
// all corners fit - add center
// all corners fit - add center
LwocOctree
node
=
getLeafNode
(
center
);
LwocOctree
node
=
getLeafNode
(
center
);
node
.
leaf
.
addMeshCenter
(
mesh
);
node
.
leaf
.
addMeshCenter
(
mesh
,
check_existed
);
// Check if leaf node requires splitting
// Check if leaf node requires splitting
if
(
node
.
leaf
.
getMeshCenters
().
size
()
>
MAX_MESH_CENTERS
)
{
if
(
node
.
leaf
.
getMeshCenters
().
size
()
>
MAX_MESH_CENTERS
)
{
if
(
node
.
hsize
>
MIN_HSIZE
)
{
if
(
node
.
hsize
>
MIN_HSIZE
)
{
if
(!
check_existed
||
!
pendingLeafNodes
.
contains
(
node
))
{
pendingLeafNodes
.
add
(
node
);
pendingLeafNodes
.
add
(
node
);
numPendingLeafNodes
.
getAndIncrement
();
}
}
}
}
}
// Now recursively add mesh to all nodes intersected by this mesh bounding box
// No
t here: No
w recursively add mesh to all nodes intersected by this mesh bounding box
return
true
;
return
true
;
}
}
public
int
addMesh
(
// returns 0 and adds to pendingMeshes if needs growing
// recursive
public
int
addMesh
(
LwocMesh
mesh
,
LwocMesh
mesh
,
boolean
check_existed
)
{
boolean
check_existed
)
{
if
(!
intersects
(
if
(!
intersects
(
...
@@ -504,4 +606,89 @@ public class LwocOctree {
...
@@ -504,4 +606,89 @@ public class LwocOctree {
return
0
;
return
0
;
}
}
/**
* Prepare list of lists to add scenes in multithreaded environment, one inner
* list for each thread.
* @param threads Array of threads, only length is used
* @return list of lists to provide to threads
*/
public
static
List
<
List
<
LwocScene
>>
getMultiPendingScenes
(
Thread
[]
threads
){
List
<
List
<
LwocScene
>>
multiPendingScenes
=
new
ArrayList
<
List
<
LwocScene
>>(
threads
.
length
);
for
(
int
i
=
0
;
i
<
threads
.
length
;
i
++)
{
multiPendingScenes
.
add
(
i
,
new
ArrayList
<
LwocScene
>());
}
return
multiPendingScenes
;
}
/**
* Combine a List of list of scenes (created in multithreaded method getMultiPendingScenes())
* into a single list.
* @param multiPendingScenes list of list of scenes
* @return flattened single list of scenes
*/
public
static
List
<
LwocScene
>
mergeMultiPendingScenes
(
List
<
List
<
LwocScene
>>
multiPendingScenes
){
List
<
LwocScene
>
pendingScenes
=
new
ArrayList
<
LwocScene
>();
for
(
List
<
LwocScene
>
scenes:
multiPendingScenes
)
{
pendingScenes
.
addAll
(
scenes
);
}
return
pendingScenes
;
}
/**
* Prepare list of lists to add meshes in multithreaded environment, one inner
* list for each thread.
* @param threads Array of threads, only length is used
* @return list of lists to provide to threads
*/
public
static
List
<
List
<
LwocMesh
>>
getMultiPendingMeshes
(
Thread
[]
threads
){
List
<
List
<
LwocMesh
>>
multiPendingMeshes
=
new
ArrayList
<
List
<
LwocMesh
>>(
threads
.
length
);
for
(
int
i
=
0
;
i
<
threads
.
length
;
i
++)
{
multiPendingMeshes
.
add
(
i
,
new
ArrayList
<
LwocMesh
>());
}
return
multiPendingMeshes
;
}
/**
* Combine a List of list of meshes (created in multithreaded method getMultiPendingMeshes())
* into a single list.
* @param multiPendingMeshes list of list of meshes
* @return flattened single list of meshes
*/
public
static
List
<
LwocMesh
>
mergeMultiPendingMeshes
(
List
<
List
<
LwocMesh
>>
multiPendingMeshes
){
List
<
LwocMesh
>
pendingMeshes
=
new
ArrayList
<
LwocMesh
>();
for
(
List
<
LwocMesh
>
meshes:
multiPendingMeshes
)
{
pendingMeshes
.
addAll
(
meshes
);
}
return
pendingMeshes
;
}
/**
* Prepare list of lists to add scenes in multithreaded environment, one inner
* list for each thread.
* @param threads Array of threads, only length is used
* @return list of lists to provide to threads
*/
public
static
List
<
List
<
LwocOctree
>>
getMultiPendingLeafNodes
(
Thread
[]
threads
){
List
<
List
<
LwocOctree
>>
multiPendingLeafNodes
=
new
ArrayList
<
List
<
LwocOctree
>>(
threads
.
length
);
for
(
int
i
=
0
;
i
<
threads
.
length
;
i
++)
{
multiPendingLeafNodes
.
add
(
i
,
new
ArrayList
<
LwocOctree
>());
}
return
multiPendingLeafNodes
;
}
/**
* Combine a List of list of scenes (created in multithreaded method getMultiPendingScenes())
* into a single list.
* @param multiPendingLeafNodes list of list of scenes
* @return flattened single list of scenes
*/
public
static
List
<
LwocOctree
>
mergeMultiPendingLeafNodes
(
List
<
List
<
LwocOctree
>>
multiPendingLeafNodes
){
List
<
LwocOctree
>
pendingLeafNodes
=
new
ArrayList
<
LwocOctree
>();
for
(
List
<
LwocOctree
>
nodes:
multiPendingLeafNodes
)
{
pendingLeafNodes
.
addAll
(
nodes
);
}
return
pendingLeafNodes
;
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment