Commit d8184ebc authored by Benedikt Zoennchen's avatar Benedikt Zoennchen
Browse files

fix wrong minX minY for CellGrid, false in cone walk in ITriConnectivity and...

fix wrong minX minY for CellGrid, false in cone walk in ITriConnectivity and use of the wrong EikonalSolver if the type is NONE.
parent 38b5f9b1
......@@ -561,7 +561,7 @@ public abstract class DefaultModel<T extends DefaultConfig> extends Observable i
new AttributesFloorField());
Function<IPoint, Double> obstacleDistance = p -> distanceField.getPotential(p, null);
IDistanceFunction distanceFunc = new DistanceFunction(bound, shapes);
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState());
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState(), bound.getMinX(), bound.getMinY());
cellGrid.pointStream().forEach(p -> cellGrid.setValue(p, new CellState(distanceFunc.apply(cellGrid.pointToCoord(p)), PathFindingTag.Reachable)));
Function<IPoint, Double> interpolationFunction = cellGrid.getInterpolationFunction();
IDistanceFunction approxDistance = p -> interpolationFunction.apply(p);
......
......@@ -9,6 +9,8 @@ import org.vadere.meshing.utils.debug.DebugGui;
import org.vadere.meshing.utils.debug.SimpleTriCanvas;
import org.vadere.util.geometry.GeometryUtils;
import org.vadere.util.geometry.shapes.IPoint;
import org.vadere.util.geometry.shapes.VCircle;
import org.vadere.util.geometry.shapes.VLine;
import org.vadere.util.geometry.shapes.VPoint;
import java.awt.*;
......@@ -1238,10 +1240,40 @@ public interface ITriConnectivity<P extends IPoint, V extends IVertex<P>, E exte
assert intersects(p1, p1.add(direction), edge);
assert getMesh().getEdges(face).contains(startVertex);
// TODO: quick solution!
VPoint q = p1.add(direction.scalarMultiply(Double.MAX_VALUE * 0.5));
VPoint q = p1.add(direction.scalarMultiply(Double.MAX_VALUE / 10_000.0));
return straightWalk2D(p1, q, face, e -> (stopCondition.test(e) || !isRightOf(q.x, q.y, e)));
}
/**
* Marching from a vertex which is the vertex (<tt>startVertex</tt>) of face (<tt>face</tt>) in the direction (<tt>direction</tt>)
* until the stop-condition (<tt>stopCondition</tt>) is fulfilled. This requires O(n) worst case time, where n is the number of faces of the mesh.
*
* <p>Assumption: The stopCondition will be fulfilled at some point.</p>
*
* @param startVertex the vertex at which the march starts
* @param face the face at which the march / search starts
* @param direction the direction in which the march will go
* @param stopCondition the stop condition at which the march will stop
* @return all visited faces in a first visited first in ordered queue, i.e. <tt>LinkedList</tt>.
*/
default LinkedList<F> straightWalk2DGather(@NotNull final E startVertex, @NotNull final F face, @NotNull final VPoint direction, @NotNull final Predicate<E> stopCondition) {
E edge = getMesh().getPrev(startVertex);
VPoint p1 = getMesh().toPoint(startVertex);
assert intersects(p1, p1.add(direction), edge);
assert getMesh().getEdges(face).contains(startVertex);
// TODO: quick solution!
VPoint q = p1.add(direction.scalarMultiply(Double.MAX_VALUE / 10_000.0));
Predicate<E> defaultStopCondion = e -> !isRightOf(q.x, q.y, e);
LinkedList<F> visitedFaces = straightGatherWalk2D(p1, q, face, defaultStopCondion.or(stopCondition));
return visitedFaces;
//return straightWalk2D(p1, q, face, e -> (stopCondition.test(e) || !isRightOf(q.x, q.y, e)));
}
default F straightWalk2D(final double x1, final double y1, @NotNull final F startFace, @NotNull final Predicate<E> stopCondition) {
return straightGatherWalk2D(x1, y1, startFace, stopCondition).peekLast();
}
......@@ -1459,16 +1491,27 @@ public interface ITriConnectivity<P extends IPoint, V extends IVertex<P>, E exte
* Get the face with the centroid closest to p and which was not visited already.
*/
Optional<F> closestFace = getMesh().streamFaces(v)
.filter(f -> !getMesh().isBorder(f))
.filter(f -> !getMesh().isBoundary(f))
.filter(f -> !visitedFaces.contains(f))
.min(Comparator.comparingDouble(f -> p.distance(getMesh().toPolygon(f).getCentroid())));
.min(Comparator.comparingDouble(f -> p.distance(getMesh().toTriangle(f).midPoint())));
/*if(!closestFace.isPresent()) {
SimpleTriCanvas canvas = SimpleTriCanvas.simpleCanvas(getMesh());
getMesh().streamFaces(v).forEach(f -> canvas.getColorFunctions().overwriteFillColor(f, Color.MAGENTA));
DebugGui.setDebugOn(true);
if(DebugGui.isDebugOn()) {
canvas.addGuiDecorator(graphics -> {
Graphics2D graphics2D = (Graphics2D)graphics;
graphics2D.setColor(Color.GREEN);
graphics2D.setStroke(new BasicStroke(0.01f));
graphics2D.draw(new VLine(q, p));
log.info("p: " + p);
graphics2D.fill(new VCircle(q, 0.05));
});
DebugGui.showAndWait(canvas);
}
}*/
SimpleTriCanvas canvas = SimpleTriCanvas.simpleCanvas(getMesh());
getMesh().streamFaces(v).forEach(f -> canvas.getColorFunctions().overwriteFillColor(f, Color.MAGENTA));
DebugGui.setDebugOn(true);
if(DebugGui.isDebugOn() && !closestFace.isPresent()) {
DebugGui.showAndWait(canvas);
}
assert closestFace.isPresent() : visitedFaces.size();
return closestFace;
......@@ -1557,7 +1600,7 @@ public interface ITriConnectivity<P extends IPoint, V extends IVertex<P>, E exte
/*if(!(getMesh().isBorder(visitedFaces.peekLast()) || contains(p.getX(), p.getY(), visitedFaces.peekLast()))) {
boolean test = contains(p.getX(), p.getY(), visitedFaces.peekLast());
}*/
assert getMesh().isBorder(visitedFaces.peekLast()) || contains(p.getX(), p.getY(), visitedFaces.peekLast());
// assert getMesh().isBorder(visitedFaces.peekLast()) || contains(p.getX(), p.getY(), visitedFaces.peekLast());
return visitedFaces;
}
......
......@@ -79,12 +79,17 @@ public class ColorFunctions
* @return gray scale color object
*/
public static <P extends IPoint, V extends IVertex<P>, E extends IHalfEdge<P>, F extends IFace<P>> Color qualityToGrayScale(final IMesh<P, V, E, F> mesh, final F face) {
float quality = (float) faceToQuality(mesh, face);
if(quality <= 1 && quality >= 0) {
return new Color(quality, quality, quality);
if(!mesh.isBoundary(face)) {
float quality = (float) faceToQuality(mesh, face);
if(quality <= 1 && quality >= 0) {
return new Color(quality, quality, quality);
}
else {
return Color.RED;
}
}
else {
return Color.RED;
return Color.WHITE;
}
}
......
......@@ -91,11 +91,12 @@ public class SimpleTriCanvas
polygon.getPoints().forEach(p -> {
graphics.setColor(Color.RED);
graphics.fill(new VCircle(p, 0.1));
graphics.fill(new VCircle(p, 0.025));
});
VPoint center = polygon.getCentroid();
graphics.drawString(Integer.toString(i), (float) center.x, (float) center.y);
graphics.fill(new VCircle(center, 0.05));
//graphics.drawString(Integer.toString(i), (float) center.x, (float) center.y);
i++;
} catch (ArrayIndexOutOfBoundsException e) {
......
......@@ -36,9 +36,9 @@ public abstract class TriCanvas
extends Canvas {
static final Logger log = LogManager.getLogger(TriCanvas.class);
static final VRectangle defaultBound = new VRectangle(0,0,10,10);
static final int defaultWidth = 500;
static final int defaultHeight = 500;
static final VRectangle defaultBound = new VRectangle(-12, -12, 24, 24);
static final int defaultWidth = 1000;
static final int defaultHeight = 1000;
protected final IMesh<P, V, E, F> mesh;
public double width;
......@@ -84,12 +84,12 @@ public abstract class TriCanvas
graphics.setColor(Color.WHITE);
graphics.fill(new VRectangle(0, 0, getWidth(), getHeight()));
Font currentFont = graphics.getFont();
Font newFont = currentFont.deriveFont(currentFont.getSize() * 0.024f);
Font newFont = currentFont.deriveFont(currentFont.getSize() * 0.010f);
graphics.setFont(newFont);
graphics.setColor(Color.GRAY);
graphics.scale(scale, scale);
graphics.translate(-bound.getMinX() + (0.5 * Math.max(0, bound.getWidth() - bound.getHeight())), -bound.getMinY() + (bound.getHeight() - height / scale));
graphics.setStroke(new BasicStroke(0.003f));
graphics.setStroke(new BasicStroke(0.001f));
graphics.setColor(Color.BLACK);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
......
......@@ -86,12 +86,16 @@ public interface IPotentialField {
Rectangle2D.Double bounds = topography.getBounds();
EikonalSolver eikonalSolver;
if(createMethod == EikonalSolverType.NONE) {
return new PotentialFieldCalculatorNone();
}
/**
* Use a regular grid based method.
*/
if(createMethod.isUsingCellGrid()) {
CellGrid cellGrid = new CellGrid(bounds.getWidth(), bounds.getHeight(),
attributesPotential.getPotentialFieldResolution(), new CellState());
attributesPotential.getPotentialFieldResolution(), new CellState(), bounds.getMinX(), bounds.getMinY());
if (createMethod != EikonalSolverType.NONE) {
for (VShape shape : targetShapes) {
......
......@@ -3,6 +3,8 @@ package org.vadere.simulator.models.potential.solver.calculators.mesh;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.vadere.meshing.utils.debug.DebugGui;
import org.vadere.meshing.utils.debug.SimpleTriCanvas;
import org.vadere.util.geometry.GeometryUtils;
import org.vadere.meshing.mesh.inter.IFace;
import org.vadere.meshing.mesh.inter.IHalfEdge;
......@@ -10,6 +12,7 @@ import org.vadere.meshing.mesh.inter.IMesh;
import org.vadere.meshing.mesh.inter.IIncrementalTriangulation;
import org.vadere.meshing.mesh.inter.IVertex;
import org.vadere.util.geometry.shapes.IPoint;
import org.vadere.util.geometry.shapes.VCircle;
import org.vadere.util.geometry.shapes.VCone;
import org.vadere.util.geometry.shapes.VLine;
import org.vadere.util.geometry.shapes.VPoint;
......@@ -25,6 +28,7 @@ import org.vadere.simulator.models.potential.solver.timecost.ITimeCostFunction;
import org.vadere.meshing.mesh.iterators.FaceIterator;
import org.vadere.util.math.IDistanceFunction;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
......@@ -40,11 +44,11 @@ import java.util.stream.Collectors;
/**
* This class computes the travelling time T(x) using the Fast Marching Method for arbitrary triangulated meshes.
* The quality of the result depends on the quality of the triangulation. Therefore, the triangualtion shouldn't contain
* This class computes the travelling time T(x) using the fast marching method for arbitrary triangulated meshes.
* The quality of the result depends on the quality of the triangulation. Therefore, the triangulation should not contain
* too many non-acute triangles.
*
* @param <P> the type of the points of the triangulation (they have to be an extension of potential points)
* @param <P> the type of the points of the triangulation extending {@link IPotentialPoint}
* @param <V> the type of the vertices of the triangulation
* @param <E> the type of the half-edges of the triangulation
* @param <F> the type of the faces of the triangulation
......@@ -53,11 +57,25 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
private static Logger logger = LogManager.getLogger(EikonalSolverFMMTriangulation.class);
private ITimeCostFunction timeCostFunction;
/**
* The time cost function defined on the geometry.
*/
private ITimeCostFunction timeCostFunction;
/**
* The triangulation the solver uses.
*/
private IIncrementalTriangulation<P, V, E, F> triangulation;
private boolean calculationFinished;
/**
* Indicates that the computation of T has been completed.
*/
private boolean calculationFinished;
/**
* The narrow-band of the fast marching method.
*/
private PriorityQueue<V> narrowBand;
private Collection<VRectangle> targetAreas;
/**
* Comparator for the heap. Vertices of points with small potentials are at the top of the heap.
......@@ -79,6 +97,7 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
/**
* Constructor for certain target points.
*
* @param timeCostFunction the time cost function t(x). Note F(x) = 1 / t(x).
* @param targetPoints Points where the propagating wave starts i.e. points that are part of the target area.
* @param triangulation the triangulation the propagating wave moves on.
......@@ -100,6 +119,7 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
/**
* Constructor for certain target shapes.
*
* @param targetShapes shapes that define the whole target area.
* @param timeCostFunction the time cost function t(x). Note F(x) = 1 / t(x).
* @param triangulation the triangulation the propagating wave moves on.
......@@ -123,9 +143,11 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
/**
* Constructor for certain vertices of the triangulation.
*
* @param timeCostFunction the time cost function t(x). Note F(x) = 1 / t(x).
* @param triangulation the triangulation the propagating wave moves on.
* @param targetVertices vertices which are part of the triangulation where the propagating wave starts i.e. points that are part of the target area.
* @param distFunc the distance function (distance to the target) which is negative inside and positive outside the area of interest
*/
public EikonalSolverFMMTriangulation(@NotNull final ITimeCostFunction timeCostFunction,
@NotNull final IIncrementalTriangulation<P, V, E, F> triangulation,
......@@ -135,7 +157,6 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
this.triangulation = triangulation;
this.calculationFinished = false;
this.timeCostFunction = timeCostFunction;
this.targetAreas = new ArrayList<>();
this.narrowBand = new PriorityQueue<>(pointComparator);
for(V vertex : targetVertices) {
......@@ -151,9 +172,9 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
narrowBand.add(vertex);
for(V v : triangulation.getMesh().getAdjacentVertexIt(vertex)) {
if(!narrowBand.contains(v)) {
P potentialP = getMesh().getPoint(v);
P potentialP = getMesh().getPoint(v);
if(potentialP.getPathFindingTag() == PathFindingTag.Undefined) {
double dist = Math.max(-distFunc.apply(potentialP), 0);
logger.info(dist);
potentialP.setPotential(Math.min(potentialP.getPotential(), dist / timeCostFunction.costAt(potentialP)));
......@@ -166,6 +187,7 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
/**
* Computes and sets the potential of all points of a face based on the distance function.
*
* @param face the face
* @param distanceFunction the distance function
*/
......@@ -202,7 +224,7 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
// unknownPenalty is ignored.
@Override
public double getPotential(IPoint pos, double unknownPenalty, double weight) {
public double getPotential(@NotNull final IPoint pos, final double unknownPenalty, final double weight) {
return weight * getPotential(pos.getX(), pos.getY());
}
......@@ -313,8 +335,8 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
* Updates a point given a triangle. The point can only be updated if the
* triangle triangleContains it and the other two points are in the frozen band.
*
* @param point
* @param face
* @param point a point for which the potential should be re-computed
* @param face a face neighbouring the point
*/
private double computePotential(@NotNull final P point, @NotNull final F face) {
// check whether the triangle does contain useful data
......@@ -328,11 +350,14 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
P p2 = getMesh().getPoint(edges.get(1));
if(isFeasibleForComputation(p1) && isFeasibleForComputation(p2)) {
if(!isNonAcute(halfEdge)) {
if(!isNonAcute(halfEdge)) {
double potential = computePotential(point, p1, p2);
//logger.info("compute potential " + potential);
return computePotential(point, p1, p2);
} // we only try to find a virtual vertex if both points are already frozen
else {
//logger.info("special case for non-acute triangle");
logger.info("special case for non-acute triangle");
Optional<P> optPoint = walkToFeasiblePoint(halfEdge, face);
if(optPoint.isPresent()) {
......@@ -372,6 +397,9 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
VPoint direction1 = pNext.subtract(p).rotate(+Math.PI/2);
VPoint direction2 = pPrev.subtract(p).rotate(-Math.PI/2);
VPoint direction11 = pNext.subtract(p);
VPoint direction22 = pPrev.subtract(p);
//logger.info(p + ", " + pNext + ", " + pPrev);
//logger.info(direction1 + ", " + direction2);
......@@ -384,7 +412,27 @@ public class EikonalSolverFMMTriangulation<P extends IPotentialPoint, V extends
Predicate<E> isEdgeInCone = e -> isPointInCone.test(e) || isPointInCone.test(getMesh().getPrev(e));
F destination = triangulation.straightWalk2D(halfEdge, face, direction1, isEdgeInCone);
LinkedList<F> visitedFaces = triangulation.straightWalk2DGather(halfEdge, face, direction2, isEdgeInCone);
F destination = visitedFaces.getLast();
SimpleTriCanvas canvas = SimpleTriCanvas.simpleCanvas(getMesh());
visitedFaces.stream().forEach(f -> canvas.getColorFunctions().overwriteFillColor(f, Color.MAGENTA));
DebugGui.setDebugOn(true);
if(DebugGui.isDebugOn()) {
// attention the view is mirrowed.
canvas.addGuiDecorator(graphics -> {
Graphics2D graphics2D = (Graphics2D)graphics;
graphics2D.setColor(Color.GREEN);
graphics2D.setStroke(new BasicStroke(0.05f));
logger.info("p: " + p);
graphics2D.draw(new VLine(p, p.add(direction1.scalarMultiply(10))));
graphics2D.setColor(Color.BLUE);
graphics2D.draw(new VLine(p, p.add(direction2.scalarMultiply(10))));
//graphics2D.fill(new VCircle(new VPoint(getMesh().toPoint(startVertex)), 0.05));
//graphics2D.fill(new VCircle(q, 0.05));
});
DebugGui.showAndWait(canvas);
}
assert !destination.equals(face);
......
......@@ -512,7 +512,7 @@ public class RealWorldPlot {
//IDistanceFunction distanceFunc = p -> -obstacleDistance.apply(p);
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState());
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState(), bound.getMinX(), bound.getMinY());
cellGrid.pointStream().forEach(p -> cellGrid.setValue(p, new CellState(distanceFunc.apply(cellGrid.pointToCoord(p)), PathFindingTag.Reachable)));
Function<IPoint, Double> interpolationFunction = cellGrid.getInterpolationFunction();
IDistanceFunction approxDistance = p -> interpolationFunction.apply(p);
......@@ -591,7 +591,7 @@ public class RealWorldPlot {
//IDistanceFunction distanceFunc = p -> -obstacleDistance.apply(p);
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState());
CellGrid cellGrid = new CellGrid(bound.getWidth(), bound.getHeight(), 0.1, new CellState(), bound.getMinX(), bound.getMinY());
cellGrid.pointStream().forEach(p -> cellGrid.setValue(p, new CellState(distanceFunc.apply(cellGrid.pointToCoord(p)), PathFindingTag.Reachable)));
Function<IPoint, Double> interpolationFunction = cellGrid.getInterpolationFunction();
IDistanceFunction approxDistance = p -> interpolationFunction.apply(p);
......
......@@ -63,7 +63,7 @@ public class PerformanceSFMM {
};
cellGrid = new CellGrid(bounds.getWidth(), bounds.getHeight(), resolution, new CellState());
cellGrid = new CellGrid(bounds.getWidth(), bounds.getHeight(), resolution, new CellState(), bounds.getMinX(), bounds.getMinY());
for (VShape shape : targets) {
FloorDiscretizer.setGridValuesForShape(cellGrid, shape, new CellState(0.0, PathFindingTag.Target));
}
......
......@@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.vadere.meshing.mesh.gen.MeshPanel;
import org.vadere.meshing.mesh.gen.PFace;
import org.vadere.meshing.mesh.gen.PHalfEdge;
import org.vadere.meshing.mesh.gen.PMesh;
......@@ -35,6 +36,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
......@@ -65,7 +67,9 @@ public class TestFFMNonUniformTriangulation {
bbox = new VRectangle(-12, -12, 24, 24);
}
private EikMesh<PotentialPoint, PVertex<PotentialPoint>, PHalfEdge<PotentialPoint>, PFace<PotentialPoint>> createEikMesh(@NotNull final IEdgeLengthFunction edgeLengthFunc, final double initialEdgeLen) {
private EikMesh<PotentialPoint, PVertex<PotentialPoint>, PHalfEdge<PotentialPoint>, PFace<PotentialPoint>> createEikMesh(
@NotNull final IEdgeLengthFunction edgeLengthFunc,
final double initialEdgeLen) {
IMeshSupplier<PotentialPoint, PVertex<PotentialPoint>, PHalfEdge<PotentialPoint>, PFace<PotentialPoint>> meshSupplier = () -> new PMesh<>((x, y) -> new PotentialPoint(x, y));
EikMesh<PotentialPoint, PVertex<PotentialPoint>, PHalfEdge<PotentialPoint>, PFace<PotentialPoint>> eikMesh = new EikMesh<>(
distanceFunc,
......@@ -78,13 +82,12 @@ public class TestFFMNonUniformTriangulation {
return eikMesh;
}
@Ignore
@Test
public void testTriangulationFMM() {
//IEdgeLengthFunction edgeLengthFunc = p -> 1.0 + 0.5*Math.min(Math.abs(distanceFunc.apply(p) + 4), Math.abs(distanceFunc.apply(p)));
//IEdgeLengthFunction unifromEdgeLengthFunc = p -> 1.0;
IEdgeLengthFunction edgeLengthFunc = p -> 1.0 + Math.abs(distanceFunc.apply(p)) * 0.5;
IEdgeLengthFunction edgeLengthFunc = p -> 1.0;
//IEdgeLengthFunction edgeLengthFunc = p -> 1.0 + Math.abs(distanceFunc.apply(p)) * 0.5;
List<VRectangle> targetAreas = new ArrayList<>();
List<IPoint> targetPoints = new ArrayList<>();
......@@ -96,7 +99,17 @@ public class TestFFMNonUniformTriangulation {
meshGenerator.generate();
triangulation = meshGenerator.getTriangulation();
//targetPoints.add(new MeshPoint(0, 0, false));
Predicate<PFace<PotentialPoint>> nonAccute = f -> triangulation.getMesh().toTriangle(f).isNonAcute();
MeshPanel meshPanel = new MeshPanel(meshGenerator.getMesh(), nonAccute, 1000, 1000, bbox);
meshPanel.display();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//targetPoints.add(new MeshPoint(0, 0, false));
VRectangle rect = new VRectangle(width / 2, height / 2, 100, 100);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment