Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
CityDoctor
CityDoctor2
Commits
7d61c01b
Commit
7d61c01b
authored
Sep 07, 2026
by
Numanoglu
Browse files
WIP multi target plane infrastructure
parent
a7866e92
Pipeline
#12437
failed with stage
in 2 minutes and 21 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healing/SweepUtilities.java
View file @
7d61c01b
...
...
@@ -8,11 +8,12 @@ import org.apache.logging.log4j.LogManager;
import
org.apache.logging.log4j.Logger
;
import
de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonDistancePlaneError
;
import
de.hft.stuttgart.citydoctor2.datastructure.Edge
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.Polygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Vertex
;
import
de.hft.stuttgart.citydoctor2.math.OrthogonalRegressionPlane
;
import
de.hft.stuttgart.citydoctor2.math.Plane
;
import
de.hft.stuttgart.citydoctor2.math.Vector3d
;
public
class
SweepUtilities
{
...
...
@@ -115,6 +116,7 @@ public class SweepUtilities {
if
(!
v
.
hasNewPoint
())
return
false
;
break
;
case
MULTI:
case
NONE:
case
ERROR:
return
false
;
...
...
@@ -150,37 +152,80 @@ public class SweepUtilities {
public
void
calcNewTargetPlane
()
{
//todo:
List
<
SweepVertex
>
multiVerts
=
new
LinkedList
<>();
// SweepVertices mit mehr als einer outgoing Edge in multiVerts-Liste sammeln,
// im Unterschied zu initStatus unabhaengig davon, ob sie auf der alten Zielebene liegen
for
(
SweepVertex
sweepVertex
:
sweepVertices
)
{
if
(
sweepVertex
.
countOutgoingEdges
()
>
1
)
{
multiVerts
.
add
(
sweepVertex
);
}
}
Plane
newTargetPlane
=
null
;
if
(
multiVerts
.
size
()
==
0
)
{
return
;
//kann eigentlich nicht passieren
}
else
if
(
multiVerts
.
size
()
==
1
)
{
//
newTargetPlane = new Plane(
Normale der alten Zielebene, multiVerts[0].get
Vertex())
newTargetPlane
=
new
Plane
(
targetPlane
.
getNormal
(),
multiVerts
.
get
(
0
).
getSweep
Vertex
())
;
}
else
if
(
multiVerts
.
size
()
==
2
)
{
// Normale entspr. Diss. von Raul berechnen
//newTargetPlane = new Plane(neue Normale, multiVerts[0].getVertex())
newTargetPlane
=
calcNewTargetPlaneFromTwoMultiVerts
(
multiVerts
);
}
else
if
(
multiVerts
.
size
()
==
3
)
{
// Wenn die drei Multi-Knoten auf einer Geraden liegen, siehe Fall ==2
// Ansonsten
// Normale einer Ebene durch die drei Punkte berechnen
//newTargetPlane = new Plane(neue Normale, multiVerts[0].getVertex())
newTargetPlane
=
calcNewTargetPlaneFromThreeMultiVerts
(
multiVerts
);
}
else
{
// Ausgleichsebene zur Liste der Multi-Knoten berechnen
// mit Methode von Matthias
newTargetPlane
=
calcRegressionPlaneForMultiVerts
(
multiVerts
);
}
if
(
newTargetPlane
!=
null
)
targetPlane
=
newTargetPlane
;
}
private
Plane
calcNewTargetPlaneFromTwoMultiVerts
(
List
<
SweepVertex
>
multiVerts
)
{
Vector3d
firstPoint
=
multiVerts
.
get
(
0
).
getSweepVertex
();
Vector3d
secondPoint
=
multiVerts
.
get
(
1
).
getSweepVertex
();
Vector3d
fixedPointDirection
=
secondPoint
.
minus
(
firstPoint
);
// Name der Toleranz hier irreführend : eigentlich ob zwei MultiKnoten unterscheidbar sind
if
(
fixedPointDirection
.
getLength
()
<
pointOnPlaneTolerance
)
{
return
new
Plane
(
targetPlane
.
getNormal
(),
firstPoint
);
}
Vector3d
sideDirection
=
targetPlane
.
getNormal
().
cross
(
fixedPointDirection
);
if
(
sideDirection
.
getLength
()
<
pointOnPlaneTolerance
)
{
return
new
Plane
(
targetPlane
.
getNormal
(),
firstPoint
);
}
Vector3d
newNormal
=
fixedPointDirection
.
cross
(
sideDirection
);
return
new
Plane
(
newNormal
,
firstPoint
);
}
private
Plane
calcNewTargetPlaneFromThreeMultiVerts
(
List
<
SweepVertex
>
multiVerts
)
{
Vector3d
firstPoint
=
multiVerts
.
get
(
0
).
getSweepVertex
();
Vector3d
secondPoint
=
multiVerts
.
get
(
1
).
getSweepVertex
();
Vector3d
thirdPoint
=
multiVerts
.
get
(
2
).
getSweepVertex
();
Vector3d
firstDirection
=
secondPoint
.
minus
(
firstPoint
);
Vector3d
secondDirection
=
thirdPoint
.
minus
(
firstPoint
);
// Normalieren?!
Vector3d
newNormal
=
firstDirection
.
cross
(
secondDirection
);
if
(
newNormal
.
getLength
()
<
pointOnPlaneTolerance
)
{
return
calcNewTargetPlaneFromTwoMultiVerts
(
multiVerts
);
}
return
new
Plane
(
newNormal
,
firstPoint
);
}
private
Plane
calcRegressionPlaneForMultiVerts
(
List
<
SweepVertex
>
multiVerts
)
{
List
<
Vector3d
>
points
=
new
ArrayList
<>(
multiVerts
.
size
());
for
(
SweepVertex
multiVert
:
multiVerts
)
{
points
.
add
(
multiVert
.
getSweepVertex
());
}
return
OrthogonalRegressionPlane
.
calculateOrthogonalRegressionPlane
(
points
);
}
private
int
countSweepVertices
(
Polygon
p
)
{
int
nVerts
=
countSweepVertices
(
p
.
getExteriorRing
());
...
...
CityDoctorParent/Extensions/CityDoctorHealer/src/main/java/de/hft/stuttgart/citydoctor2/healing/SweepVertex.java
View file @
7d61c01b
...
...
@@ -149,6 +149,7 @@ public class SweepVertex {
newPoint
=
intersectionPoint
;
return
;
case
MULTI:
case
NONE:
case
ERROR:
default
:
...
...
CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/SweepUtilitiesMultiVertexTest.java
0 → 100644
View file @
7d61c01b
package
de.hft.stuttgart.citydoctor2.healing
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
static
org
.
junit
.
Assert
.
assertTrue
;
import
org.junit.Test
;
import
de.hft.stuttgart.citydoctor2.check.error.NonPlanarPolygonDistancePlaneError
;
import
de.hft.stuttgart.citydoctor2.datastructure.Building
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
import
de.hft.stuttgart.citydoctor2.datastructure.Polygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Vertex
;
import
de.hft.stuttgart.citydoctor2.math.Plane
;
import
de.hft.stuttgart.citydoctor2.math.Vector3d
;
public
class
SweepUtilitiesMultiVertexTest
{
/**
* Ein Multi-Knoten entsteht hier durch ein kleines Zusatzdreieck, das mit der
* Sweep-Fläche nur den Außenringpunkt an Index 0 teilt. Dadurch besitzt dieser
* Punkt zwei abführende Kanten. calcNewTargetPlane() soll die Zielebene parallel
* zur alten Ebene durch diesen Multi-Knoten legen, so dass er danach ON_PLANE
* ist.
*/
@Test
public
void
initializeMovesTargetPlaneThroughOneMultiVertex
()
{
Building
building
=
SyntheticSweepPlanarityModelFactory
.
createSweepCase3OneMultiVertex
();
Polygon
sweepPolygon
=
getPolygonAtIndex
(
building
,
0
);
Vertex
multiVertex
=
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
0
);
SweepUtilities
utilities
=
createSweepUtilitiesWithDistantHorizontalTargetPlane
(
sweepPolygon
);
System
.
out
.
println
();
System
.
out
.
println
(
"=== Multi-Knoten: Zielebene durch einen komplexen Punkt legen ==="
);
System
.
out
.
println
(
"Modell: Außenring Index 0 teilt sich einen Punkt mit einem Zusatzdreieck."
);
System
.
out
.
println
(
"Erwartung: Dieser Punkt hat 2 abführende Kanten und wird nach der neuen Zielebene ON_PLANE."
);
printPoint
(
"Testknoten Außenring[0]"
,
multiVertex
);
utilities
.
initialize
();
SweepVertex
sweepVertex
=
utilities
.
getSweepVertices
().
get
(
0
);
System
.
out
.
println
(
"Berechnet: Status "
+
sweepVertex
.
getStatus
()
+
", abführende Kanten "
+
sweepVertex
.
countOutgoingEdges
()
+
"."
);
System
.
out
.
println
();
assertEquals
(
multiVertex
,
sweepVertex
.
getSweepVertex
());
assertEquals
(
2
,
sweepVertex
.
countOutgoingEdges
());
assertEquals
(
SweepVertexStatus
.
ON_PLANE
,
sweepVertex
.
getStatus
());
}
/**
* Zwei Multi-Knoten liegen an den Außenringpunkten 0 und 1. Die neue Zielebene
* soll durch beide Punkte laufen. Damit bleiben genau diese problematischen
* Fall-3-Knoten nach der zweiten Statusermittlung unbewegt auf der Zielebene.
*/
@Test
public
void
initializeMovesTargetPlaneThroughTwoMultiVertices
()
{
Building
building
=
SyntheticSweepPlanarityModelFactory
.
createSweepCase3TwoMultiVertices
();
Polygon
sweepPolygon
=
getPolygonAtIndex
(
building
,
0
);
Vertex
firstMultiVertex
=
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
0
);
Vertex
secondMultiVertex
=
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
1
);
SweepUtilities
utilities
=
createSweepUtilitiesWithDistantHorizontalTargetPlane
(
sweepPolygon
);
System
.
out
.
println
();
System
.
out
.
println
(
"=== Multi-Knoten: Zielebene durch zwei komplexe Punkte legen ==="
);
System
.
out
.
println
(
"Modell: Außenring Indizes 0 und 1 haben je ein Zusatzdreieck."
);
System
.
out
.
println
(
"Erwartung: Beide Punkte haben 2 abführende Kanten und werden nach der neuen Zielebene ON_PLANE."
);
printPoint
(
"Testknoten Außenring[0]"
,
firstMultiVertex
);
printPoint
(
"Testknoten Außenring[1]"
,
secondMultiVertex
);
utilities
.
initialize
();
SweepVertex
firstSweepVertex
=
utilities
.
getSweepVertices
().
get
(
0
);
SweepVertex
secondSweepVertex
=
utilities
.
getSweepVertices
().
get
(
1
);
System
.
out
.
println
(
"Berechnet Außenring[0]: Status "
+
firstSweepVertex
.
getStatus
()
+
", abführende Kanten "
+
firstSweepVertex
.
countOutgoingEdges
()
+
"."
);
System
.
out
.
println
(
"Berechnet Außenring[1]: Status "
+
secondSweepVertex
.
getStatus
()
+
", abführende Kanten "
+
secondSweepVertex
.
countOutgoingEdges
()
+
"."
);
System
.
out
.
println
();
assertMultiVertexOnPlane
(
firstSweepVertex
,
firstMultiVertex
);
assertMultiVertexOnPlane
(
secondSweepVertex
,
secondMultiVertex
);
}
/**
* Drei Multi-Knoten liegen an den Außenringpunkten 0, 1 und 2. Die neue
* Zielebene kann dadurch direkt aus drei nicht-kollinearen Punkten konstruiert
* werden. Diese drei Knoten sollen anschließend ON_PLANE sein.
*/
@Test
public
void
initializeMovesTargetPlaneThroughThreeMultiVertices
()
{
Building
building
=
SyntheticSweepPlanarityModelFactory
.
createSweepCase3ThreeMultiVertices
();
Polygon
sweepPolygon
=
getPolygonAtIndex
(
building
,
0
);
SweepUtilities
utilities
=
createSweepUtilitiesWithDistantHorizontalTargetPlane
(
sweepPolygon
);
System
.
out
.
println
();
System
.
out
.
println
(
"=== Multi-Knoten: Zielebene durch drei komplexe Punkte legen ==="
);
System
.
out
.
println
(
"Modell: Außenring Indizes 0, 1 und 2 haben je ein Zusatzdreieck."
);
System
.
out
.
println
(
"Erwartung: Die drei Punkte bestimmen die neue Zielebene und werden ON_PLANE."
);
printPoint
(
"Testknoten Außenring[0]"
,
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
0
));
printPoint
(
"Testknoten Außenring[1]"
,
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
1
));
printPoint
(
"Testknoten Außenring[2]"
,
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
2
));
utilities
.
initialize
();
for
(
int
i
=
0
;
i
<
3
;
i
++)
{
SweepVertex
sweepVertex
=
utilities
.
getSweepVertices
().
get
(
i
);
Vertex
expectedVertex
=
sweepPolygon
.
getExteriorRing
().
getVertices
().
get
(
i
);
System
.
out
.
println
(
"Berechnet Außenring["
+
i
+
"]: Status "
+
sweepVertex
.
getStatus
()
+
", abführende Kanten "
+
sweepVertex
.
countOutgoingEdges
()
+
"."
);
assertMultiVertexOnPlane
(
sweepVertex
,
expectedVertex
);
}
System
.
out
.
println
();
}
/**
* Vier Multi-Knoten treiben den Regressionsebenen-Zweig an. In diesem
* synthetischen Modell sind die vier Eckpunkte absichtlich nicht exakt koplanar.
* Die Regressionsebene kann daher nicht alle Multi-Knoten exakt fixieren; nach
* der zweiten Statusermittlung muss mindestens ein Multi-Knoten ERROR werden,
* solange die echte Fall-3-Topologie noch fehlt.
*/
@Test
public
void
initializeUsesRegressionPlaneForFourMultiVerticesAndLeavesUnresolvedOnesAsError
()
{
Building
building
=
SyntheticSweepPlanarityModelFactory
.
createSweepCase3FourMultiVertices
();
Polygon
sweepPolygon
=
getPolygonAtIndex
(
building
,
0
);
SweepUtilities
utilities
=
createSweepUtilitiesWithDistantHorizontalTargetPlane
(
sweepPolygon
);
System
.
out
.
println
();
System
.
out
.
println
(
"=== Multi-Knoten: vier komplexe Punkte erzwingen Regressionsebene ==="
);
System
.
out
.
println
(
"Modell: Alle vier Außenringpunkte haben Zusatzdreiecke und sind nicht exakt koplanar."
);
System
.
out
.
println
(
"Erwartung: Alle vier haben 2 abführende Kanten; mindestens einer bleibt nach der Regression ERROR."
);
utilities
.
initialize
();
int
errorCount
=
0
;
for
(
int
i
=
0
;
i
<
4
;
i
++)
{
SweepVertex
sweepVertex
=
utilities
.
getSweepVertices
().
get
(
i
);
printPoint
(
"Testknoten Außenring["
+
i
+
"]"
,
sweepVertex
.
getSweepVertex
());
System
.
out
.
println
(
"Berechnet Außenring["
+
i
+
"]: Status "
+
sweepVertex
.
getStatus
()
+
", abführende Kanten "
+
sweepVertex
.
countOutgoingEdges
()
+
"."
);
assertEquals
(
2
,
sweepVertex
.
countOutgoingEdges
());
if
(
sweepVertex
.
getStatus
()
==
SweepVertexStatus
.
ERROR
)
{
errorCount
++;
}
}
System
.
out
.
println
(
"Berechnet: "
+
errorCount
+
" Multi-Knoten bleiben ERROR."
);
System
.
out
.
println
();
assertTrue
(
errorCount
>
0
);
}
private
void
assertMultiVertexOnPlane
(
SweepVertex
sweepVertex
,
Vertex
expectedVertex
)
{
assertEquals
(
expectedVertex
,
sweepVertex
.
getSweepVertex
());
assertEquals
(
2
,
sweepVertex
.
countOutgoingEdges
());
assertEquals
(
SweepVertexStatus
.
ON_PLANE
,
sweepVertex
.
getStatus
());
}
private
SweepUtilities
createSweepUtilitiesWithDistantHorizontalTargetPlane
(
Polygon
sweepPolygon
)
{
Plane
initialTargetPlane
=
new
Plane
(
new
Vector3d
(
0
,
0
,
1
),
new
Vector3d
(
0
,
0
,
10
));
NonPlanarPolygonDistancePlaneError
error
=
new
NonPlanarPolygonDistancePlaneError
(
sweepPolygon
,
0
,
null
,
initialTargetPlane
);
return
new
SweepUtilities
(
error
,
0.001
,
0.001
);
}
private
Polygon
getPolygonAtIndex
(
Building
building
,
int
index
)
{
Geometry
geometry
=
building
.
getGeometries
().
get
(
0
);
return
geometry
.
getPolygons
().
get
(
index
);
}
private
void
printPoint
(
String
label
,
Vector3d
point
)
{
System
.
out
.
println
(
label
+
": x="
+
point
.
getX
()
+
", y="
+
point
.
getY
()
+
", z="
+
point
.
getZ
());
}
}
CityDoctorParent/Extensions/CityDoctorHealer/src/test/java/de/hft/stuttgart/citydoctor2/healing/SyntheticSweepPlanarityModelFactory.java
View file @
7d61c01b
...
...
@@ -57,6 +57,26 @@ public class SyntheticSweepPlanarityModelFactory {
createSweepCase2ExtendingEdge
(),
new
File
(
"sweep-case2-extending-edge.gml"
),
"SweepCase2ExtendingEdge"
);
exportForGui
(
createSweepCase3OneMultiVertex
(),
new
File
(
"sweep-case3-one-multi-vertex.gml"
),
"SweepCase3OneMultiVertex"
);
exportForGui
(
createSweepCase3TwoMultiVertices
(),
new
File
(
"sweep-case3-two-multi-vertices.gml"
),
"SweepCase3TwoMultiVertices"
);
exportForGui
(
createSweepCase3ThreeMultiVertices
(),
new
File
(
"sweep-case3-three-multi-vertices.gml"
),
"SweepCase3ThreeMultiVertices"
);
exportForGui
(
createSweepCase3FourMultiVertices
(),
new
File
(
"sweep-case3-four-multi-vertices.gml"
),
"SweepCase3FourMultiVertices"
);
}
static
Building
createNonPlanarCube
(
double
boxsize
,
double
bottomerr
,
double
topLift
)
{
...
...
@@ -243,6 +263,44 @@ public class SyntheticSweepPlanarityModelFactory {
return
createSweepCase2SingleOutgoingEdge
(
new
Vertex
(
1
,
2
,
0
));
}
/**
* Fall-3-Modell: Genau eine Ecke des nicht-planaren Sweep-Polygons besitzt
* zwei abführende Kanten. Damit wird der erste calcNewTargetPlane-Zweig
* sichtbar: Die neue Zielebene bleibt parallel zur alten und läuft durch diesen
* einen Multi-Knoten.
*/
static
Building
createSweepCase3OneMultiVertex
()
{
return
createSweepCase3MultiVertexPolygon
(
true
,
false
,
false
,
false
);
}
/**
* Fall-3-Modell: Zwei Ecken des nicht-planaren Sweep-Polygons besitzen jeweils
* zwei abführende Kanten. Damit wird die Zielebene durch ein festes Punktepaar
* gelegt und möglichst nah an der alten Ebenenrichtung gehalten.
*/
static
Building
createSweepCase3TwoMultiVertices
()
{
return
createSweepCase3MultiVertexPolygon
(
true
,
true
,
false
,
false
);
}
/**
* Fall-3-Modell: Drei Ecken des nicht-planaren Sweep-Polygons besitzen jeweils
* zwei abführende Kanten. Das ist der direkte Drei-Punkte-Ebenenfall; bei
* Kollinearität fällt die Implementierung auf die Zwei-Punkte-Konstruktion
* zurück.
*/
static
Building
createSweepCase3ThreeMultiVertices
()
{
return
createSweepCase3MultiVertexPolygon
(
true
,
true
,
true
,
false
);
}
/**
* Fall-3-Modell: Alle vier Ecken des nicht-planaren Sweep-Polygons sind
* Multi-Knoten. Damit wird der Zweig angestoßen, der nur aus den Multi-Knoten
* eine Regressionsebene berechnet.
*/
static
Building
createSweepCase3FourMultiVertices
()
{
return
createSweepCase3MultiVertexPolygon
(
true
,
true
,
true
,
true
);
}
private
static
Building
createSweepCase2SingleOutgoingEdge
(
Vertex
outgoingTarget
)
{
Building
building
=
new
Building
();
Geometry
geom
=
new
Geometry
(
GeometryType
.
MULTI_SURFACE
,
Lod
.
LOD2
,
Orientation
.
OUTWARD
);
...
...
@@ -259,6 +317,43 @@ public class SyntheticSweepPlanarityModelFactory {
geom
.
updateEdgesAndVertices
();
return
building
;
}
private
static
Building
createSweepCase3MultiVertexPolygon
(
boolean
firstIsMulti
,
boolean
secondIsMulti
,
boolean
thirdIsMulti
,
boolean
fourthIsMulti
)
{
Building
building
=
new
Building
();
Geometry
geom
=
new
Geometry
(
GeometryType
.
MULTI_SURFACE
,
Lod
.
LOD2
,
Orientation
.
OUTWARD
);
building
.
addGeometry
(
geom
);
Vertex
lowerLeft
=
new
Vertex
(
0
,
0
,
0
);
Vertex
lowerRight
=
new
Vertex
(
4
,
0
,
0.4
);
Vertex
upperRight
=
new
Vertex
(
4
,
4
,
-
0.3
);
Vertex
upperLeft
=
new
Vertex
(
0
,
4
,
0.2
);
addPolygon
(
geom
,
lowerLeft
,
lowerRight
,
upperRight
,
upperLeft
);
if
(
firstIsMulti
)
{
addMultiVertexWing
(
geom
,
lowerLeft
,
-
1.2
,
-
0.8
);
}
if
(
secondIsMulti
)
{
addMultiVertexWing
(
geom
,
lowerRight
,
1.2
,
-
0.8
);
}
if
(
thirdIsMulti
)
{
addMultiVertexWing
(
geom
,
upperRight
,
1.2
,
0.8
);
}
if
(
fourthIsMulti
)
{
addMultiVertexWing
(
geom
,
upperLeft
,
-
1.2
,
0.8
);
}
geom
.
updateEdgesAndVertices
();
return
building
;
}
private
static
void
addMultiVertexWing
(
Geometry
geom
,
Vertex
anchor
,
double
dx
,
double
dy
)
{
addPolygon
(
geom
,
anchor
,
new
Vertex
(
anchor
.
getX
()
+
dx
,
anchor
.
getY
(),
anchor
.
getZ
()
+
1.2
),
new
Vertex
(
anchor
.
getX
(),
anchor
.
getY
()
+
dy
,
anchor
.
getZ
()
-
1.0
));
}
private
static
void
exportForGui
(
Building
building
,
File
output
,
String
gmlId
)
throws
CityDoctorWriteException
{
CityDoctorModel
model
=
createCityDoctorModel
(
building
,
output
,
gmlId
);
...
...
Write
Preview
Supports
Markdown
0%
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!
Cancel
Please
register
or
sign in
to comment