Starting from 2021-07-01, all LRZ GitLab users will be required to explicitly accept the GitLab Terms of Service. Please see the detailed information at https://doku.lrz.de/display/PUBLIC/GitLab and make sure that your projects conform to the requirements.

Commit 9f4c3b9a authored by Marion Goedel's avatar Marion Goedel
Browse files

Merge branch 'master' of https://gitlab.lrz.de/vadere/vadere

parents da5638e7 40bf0d23
......@@ -43,7 +43,7 @@ unit_tests_with_coverage:
script:
- Documentation/version-control/git-hook-vadere-software
- mvn clean
- mvn -Dtest=!TestConvolution,!TestBitonicSort,!TestCLLinkedList test
- mvn -Dtest=!TestConvolution,!TestBitonicSort,!TestCLLinkedList,!TestCLOptimalStepsModel test
- python3 Tools/ContinuousIntegration/collect_line_and_branch_coverage.py
run_scenario_files:
......
......@@ -2,6 +2,13 @@
# In Progress: v0.7
## Added
- In package `org.vadere.simulator.util`, added `TopographyChecker` to show user if the current topography contains problems. The
`TopographyChecker` will check for overlapping `ScenarioElements` and check for inconsistence settings such as missing
TargetIDs for sources or inconsistenct speed ranges for pedestrians. See this [table](Documentation/changelLogImages/TopographyCheckerMessages.md) for supported warnings and erros
as well as this [picture](Documentation/changelLogImages/TopographyChecker.png) to see which kind of overlap produces erros or warnings.
# v0.6 (2018-09-07)
## Added
......
# TopographyChecker Messages
## Overlapping ScenarioElements
|Type of Elemetns | TotalOvelrap/ contained in | PartialOverlap |
|--------------------|:----------------:|:----------------:|
|Obstacle / Obstacle | WARN | ./. |
|Obstacle / Source | ERROR | ERROR |
|Obstacle / Target | ERROR | WARN |
|Obstacle / Stairs | ERROR | WARN |
|Obstacle / Ped. | ERROR | ERROR |
|Source / Source | WARN | WARN |
|Source / Target | WARN | WARN |
|Source / Stairs | WARN | WARN |
|Target / Target | WARN | WARN |
|Target / Stairs | WARN | WARN |
|Stairs / Stairs | ERROR | ERROR |
Legend:
WARN Warning but simulation is possible
ERROR Error and simulation is not possible
./. Nothing to do all good
## Misc Tests
### Errors
- A Source has no targetId set but is setup to spawn pedestrians. Solution: Set targetId
- A Source has a targetId set but the target does not exist. Solution: Create target or remove unused targetId from Source.
- If the SpeedDistributionMean of a pedestrian ist not between min/max speed.
### Warnings
- A Source has no targetId set and does not spawn pedestrians. Solution: Possible error
- A Target is never used by any Source. Solution: Possible error and this will cost performance.
- The Stairs model only works for 'normal' one-step-stairs. If the tread is outside of
the range of 10cm < x < 35cm. The simulation does not make sense.
- If the speed setup of a pedestrian is bigger then 12.0 m/s (world record). This is possible an error.
......@@ -82,7 +82,7 @@
<configuration>
<archive>
<manifest>
<mainClass>org.vadere.simulator.entrypoints.VadereConsole</mainClass>
<mainClass>org.vadere.simulator.entrypoints.cmd.VadereConsole</mainClass>
</manifest>
</archive>
<descriptorRefs>
......
......@@ -217,6 +217,7 @@ PostVis.chbHidePedAtTarget.text=Hide Pedestrians at Target
PostVis.chbHideTrajAtTarget.text=Hide Trajectories at Target
PostVis.chbCleanSnapshot.text=Hide Trajectories on Snapshots
SettingsDialog.chbUseRandomColors.text=Random Coloring
SettingsDialog.chbHideVoronoiDiagram.text=Hide VoronoiDiagram
SettingsDialog.chbShowObstacles.text=Show Obstacles
SettingsDialog.chbShowTargets.text=Show Targets
......@@ -297,9 +298,38 @@ TopographyCreator.btnConvexHull.label=Convex Hull
TopographyCreator.btnSimplePolygon.label=Simple Polygon
TopographyCreator.btnCircle.label=Circle
TopographyCreator.btnRectangle.label=Rectangle
TopographyCreator.btnChecker.tooltip=Show Topography Checker Messages
TopographyCreator.btnMakro.tooltip=Generate not set IDs.
select_shape_tooltip=Select Shape
# TopographyChecker
TopographyChecker.type.error=Error
TopographyChecker.type.warning=Warning
TopographyChecker.source.targetIdNotFound=The following target ids where not found in the scenario.
TopographyChecker.source.noTargetIdSet=No Target Ids set for Source.
TopographyChecker.source.noTargetIdAndNoSpawn=No Target Ids set for Source with SpawnNumber 0. This might be an error.
TopographyChecker.source.idNotUnique=Multiple Sources have the same ID.
TopographyChecker.stairs.wrongTreadDim=Stair treadDepth outside of allowed dimension.
TopographyChecker.target.unused=The target is not used in any source. Remove target to increase performance.
TopographyChecker.pedestrian.speedsetup=speedDistributionMean must be within min/max range.
TopographyChecker.pedestrian.speedNotLogical=Min or Max speed of pedestrian is bigger than world record.
TopographyChecker.pedestrian.speedIsNegative=Min or Max speed is negative.
TopographyChecker.overlap.stair.stair=Stairs cannot overlap.
TopographyChecker.overlap.target.stair=A target and a stairs element overlap in scenario.
TopographyChecker.overlap.source.stair=A source and a stairs element overlap in scenario.
TopographyChecker.overlap.source.target=A source and a target element overlap in scenario.
TopographyChecker.overlap.source.source=Two source overlap. This can cause overlapping when useFreeSpaceOnly is not set.
TopographyChecker.overlap.target.target=Two targets overlap.
TopographyChecker.overlap.obstacle.stairs.err=Stairs is completely contained in an obstacle.
TopographyChecker.overlap.obstacle.stairs.warn=Stairs contains an obstacle. This setup works put is not tested. Split in two stairs.
TopographyChecker.overlap.obstacle.target.err=Target is completely contained in an obstacle.
TopographyChecker.overlap.obstacle.target.warn=Target contains an obstacle. This setup works but reduces the actual size of the target.
TopographyChecker.overlap.obstacle.source=Obstacle overlaps with a source. This setup will spawn pedestrians within the obstacle.
TopographyChecker.overlap.obstacle.obstacle=Two obstacles overlap / is enclosed completely. Delete one for better performance.
# tab titles Topography creator / postvis
Tab.Simulation.title=Simulation
Tab.Model.title=Model
......@@ -334,8 +364,12 @@ ActionSeeDiscardChanges.popup.title=Unsaved changes
SaveDespiteJsonErrors.title=JSON-Errors.
SaveDespiteJsonErrors.text=There are errors in the JSON of the currently open scenario. Do you want to correct those?
SaveDespiteTopographyCheckerErrors.title=Topography-Errors.
SaveDespiteTopographyCheckerErrors.text=There are errors in the Topography of the currently open scenario. Do you want to correct those?
RunScenarioJsonErrors.title=Resolve JSON errors
RunScenarioJsonErrors.text=The simulation can't be started with erroneous JSON.
RunScenarioTopographyCheckerErrors.title=Resolve TopographyChecker errors
RunScenarioTopographyCheckerErrors.text=The simulation can't be started with erroneous Topography setup.
RunScenarioNotReadyToRun.text=One or more of the selected scenarios isn't ready to run.
ActionEditScenarioDescription.menu.title=Edit description
......@@ -215,6 +215,7 @@ PostVis.chbHidePedAtTarget.text=Fu\u00dfg\u00E4nger im Ziel nicht anzeigen
PostVis.chbHideTrajAtTarget.text=Trajektorien am Ziel nicht anzeigen
PostVis.chbCleanSnapshot.text=Trajektorien auf Snapshots nicht anzeigen
SettingsDialog.chbUseRandomColors.text=Zuf\u00E4llige Farben
SettingsDialog.chbHideVoronoiDiagram.text=Voronoi-Diagramm nicht anzeigen
SettingsDialog.chbShowObstacles.text=Hindernisse anzeigen
SettingsDialog.chbShowTargets.text=Ziele anzeigen
......@@ -296,6 +297,34 @@ TopographyCreator.btnConvexHull.label=Convexe H\u00fclle
TopographyCreator.btnSimplePolygon.label=Einfaches Polygon
TopographyCreator.btnCircle.label=Kreis
TopographyCreator.btnRectangle.label=Rechteck
TopographyCreator.btnChecker.tooltip=Topography Linter Nachrichten
TopographyCreator.btnMakro.tooltip=Erzeuge nicht gesetzte IDs.
# TopographyChecker
TopographyChecker.type.error=Fehler
TopographyChecker.type.warning=Warnung
TopographyChecker.source.targetIdNotFound=Die folgenden Ziel-IDs wurden nicht im Szenario gefunden.
TopographyChecker.source.noTargetIdSet=In der Quelle wurden keine Ziel Ids vergeben.
TopographyChecker.source.noTargetIdAndNoSpawn=In der Quelle wurden keine Ziel Ids vergeben, aber die Spawn Anzahl ist bei 0.
TopographyChecker.source.idNotUnique=Quellen haben keine eindeutige ID.
TopographyChecker.stairs.wrongTreadDim=Stufentiefe ist au\u00dferhalb des Definitionsbereichs.
TopographyChecker.target.unused=Das Ziel wird von keiner Quelle verwendet. Entferne das Ziel um die Performance zu erh\u00f6hen
TopographyChecker.pedestrian.speedsetup=speedDistributionMean muss im Bereich min/max liegen.
TopographyChecker.pedestrian.speedNotLogical=Min or Max Geschwindigkeit ist gr\u00f6\u00dfer als Weltrekord.
TopographyChecker.pedestrian.speedIsNegative=Min or Max Geschwindigkeit ist negativ.
TopographyChecker.overlap.stair.stair=Treppen d\u00fcrfen nicht \u00fcberlappen.
TopographyChecker.overlap.target.stair=Ein Ziel und eine Treppen \u00fcberlappen im Scenario.
TopographyChecker.overlap.source.stair=Eine Quelle und eine Treppe \u00fcberlappen im Scenario.
TopographyChecker.overlap.source.target=Eine Quelle und eine Ziel \u00fcberappen im Scenario.
TopographyChecker.overlap.source.source=Zwei Quellen \u00fcberlappen. Wenn useFreeSpaceOnly nicht gesetzt ist kann dies zu \u00fcberlappungen von Pedestrians f\u00fchren.
TopographyChecker.overlap.target.target=Zwei Ziele \u00fcberlappen.
TopographyChecker.overlap.obstacle.stairs.err=Treppe ist komplett in einem Hinderniss eingeschossen.
TopographyChecker.overlap.obstacle.stairs.warn=Treppe hat ein Hinderiss in sich. Dies ist m\u00f6glich aber nicht getestet. Wenn m\u00f6glich in zwei treppen aufteilen.
TopographyChecker.overlap.obstacle.target.err=Ziel ist komplett in einem Hinderniss eingeschlossen
TopographyChecker.overlap.obstacle.target.warn=Ziel hat ein Hinderniss in sich. Dies ist m\u00f6glich reduziert aber die tats\u00c4chliche Fl\u00c4che des Ziels.
TopographyChecker.overlap.obstacle.source=Hinderniss \u00fcberdeckt Quelle. Die f\u00fchrt dazu, dass Pedestrians im Hinderniss erzeugt werden.
TopographyChecker.overlap.obstacle.obstacle=Zwei identische bzw. eingeschlossene Obstacles gefunden. L\u00f6sche eins f\u00fcr bessere Performance
# tab titles Topography creator / postvis
Tab.Simulation.title=Simulation
......@@ -331,8 +360,12 @@ ActionSeeDiscardChanges.popup.title=Ungespeicherte \u00c4nderungen
SaveDespiteJsonErrors.title=Fehlerhaftes JSON.
SaveDespiteJsonErrors.text=Im ge\u00f6ffneten Szenario bestehen Fehler im JSON. Wollen Sie diese korrigieren?
SaveDespiteTopographyCheckerErrors.title=Fehlerhafte Topography.
SaveDespiteTopographyCheckerErrors.text=Im ge\u00f6ffneten Szenario bestehen Fehler in der Topography. Wollen Sie diese korrigieren?
RunScenarioJsonErrors.title=JSON Fehler beheben
RunScenarioJsonErrors.text=Die Simulation kann nicht mit fehlerhaftem JSON gestartet werden.
RunScenarioTopographyCheckerErrors.title=TopographyChecker Fehler beheben
RunScenarioTopographyCheckerErrors.text=Die Simulation kann nicht mit einer fehlerhaften Topography gestartet werden.
RunScenarioNotReadyToRun.text=Eines oder mehrere der ausgew\u00e4hlten Szenario sind derzeit nicht lauff\u00e4hig.
ActionEditScenarioDescription.menu.title=Beschreibung bearbeiten
......@@ -66,7 +66,7 @@ public abstract class DefaultModel<T extends DefaultConfig> extends Observable i
this.cursorWorldPosition = VPoint.ZERO;
this.selectScenarioElementListener = new LinkedList<>();
this.voronoiDiagram = null;
this.showVoroniDiagram = false;
this.showVoroniDiagram = true;
this.showSelection = false;
this.mouseSelectionMode = new DefaultSelectionMode(this);
this.viewportChangeListeners = new ArrayList<>();
......
......@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.vadere.gui.components.utils.ColorHelper;
import org.vadere.gui.components.utils.Resources;
public class DefaultSimulationConfig extends DefaultConfig {
......@@ -16,6 +17,7 @@ public class DefaultSimulationConfig extends DefaultConfig {
private double densityStandardDerivation = Double.valueOf(resources.getProperty("Density.standardderivation"));
private double pedestrianTorso = Double.valueOf(resources.getProperty("Pedestrian.Radius")) * 2;
private boolean useRandomPedestrianColors = false;
private boolean showPedestrianIds = false;
private boolean showTargets = true;
private boolean showSources = true;
......@@ -31,6 +33,7 @@ public class DefaultSimulationConfig extends DefaultConfig {
private boolean showGroups = false;
protected final Color pedestrianDefaultColor = Color.BLUE;
private Map<Integer, Color> pedestrianColors = new TreeMap<>();
private Map<Integer, Color> randomColors = new HashMap<>();
private double gridWidth = Double.valueOf(resources.getProperty("ProjectView.cellWidth"));
private final double MIN_CELL_WIDTH = Double.valueOf(resources.getProperty("ProjectView.minCellWidth"));
private final double MAX_CELL_WIDTH = Double.valueOf(resources.getProperty("ProjectView.maxCellWidth"));
......@@ -42,6 +45,7 @@ public class DefaultSimulationConfig extends DefaultConfig {
public DefaultSimulationConfig(final DefaultSimulationConfig config) {
super(config);
this.randomColors = new HashMap<>();
this.pedestrianColors = new HashMap<>();
for (Map.Entry<Integer, Color> entry : config.pedestrianColors.entrySet()) {
......@@ -219,6 +223,25 @@ public class DefaultSimulationConfig extends DefaultConfig {
}
}
public void clearRandomColors() {
randomColors.clear();
}
public Color getRandomColor(int pedId) {
if (!randomColors.containsKey(pedId)) {
randomColors.put(pedId, ColorHelper.randomColor());
}
return randomColors.get(pedId);
}
public void setUseRandomPedestrianColors(final boolean useRandomPedestrianColors) {
this.useRandomPedestrianColors = useRandomPedestrianColors;
}
public boolean isUseRandomPedestrianColors() {
return useRandomPedestrianColors;
}
public void setGridWidth(double gridWidth) {
this.gridWidth = gridWidth;
}
......
package org.vadere.gui.components.model;
import java.awt.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.vadere.state.scenario.Agent;
......@@ -87,6 +90,9 @@ public abstract class SimulationModel<T extends DefaultSimulationConfig> extends
if (config.hasChanged()) {
setChanged();
config.clearChange();
if(!config.isUseRandomPedestrianColors()) {
config.clearRandomColors();
}
}
// }
super.notifyObservers();
......
......@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class ColorHelper {
......@@ -20,10 +21,16 @@ public class ColorHelper {
private int maxValue;
private static Random random = new Random();
public ColorHelper(final int maxValue) {
this.maxValue = maxValue;
}
public static Color randomColor() {
return new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
public Color numberToColor(final double value) {
if (value < 0) {
return numberToColorPercentage(0);
......
package org.vadere.gui.components.view;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.io.IOException;
import java.io.InputStream;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.vadere.simulator.entrypoints.ReflectionAttributeModifier;
import org.vadere.gui.components.model.IDefaultModel;
import org.vadere.gui.projectview.view.JsonValidIndicator;
import org.vadere.gui.projectview.view.ProjectView;
import org.vadere.gui.projectview.view.ScenarioPanel;
import org.vadere.gui.topographycreator.model.AgentWrapper;
import org.vadere.gui.topographycreator.model.TopographyCreatorModel;
import org.vadere.simulator.util.TopographyChecker;
import org.vadere.state.attributes.Attributes;
import org.vadere.state.scenario.Pedestrian;
import org.vadere.state.scenario.ScenarioElement;
import org.vadere.state.util.StateJsonConverter;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* The ScenarioElementView display's a ScenarioElement in JSON-Format.
......@@ -48,26 +42,26 @@ public class ScenarioElementView extends JPanel implements ISelectScenarioElemen
private JsonValidIndicator jsonValidIndicator;
public ScenarioElementView(final IDefaultModel defaultModel) {
this(defaultModel, null);
this(defaultModel,null, null);
}
public ScenarioElementView(final IDefaultModel defaultModel, final Component topComponent) {
public ScenarioElementView(final IDefaultModel defaultModel, final JsonValidIndicator jsonValidIndicator, final Component topComponent) {
this.panelModel = defaultModel;
this.panelModel.addSelectScenarioElementListener(this);
this.jsonValidIndicator = jsonValidIndicator;
CellConstraints cc = new CellConstraints();
JScrollPane scrollPane = new JScrollPane();
scrollPane.setPreferredSize(new Dimension(1, Toolkit.getDefaultToolkit().getScreenSize().height));
if (topComponent != null) {
if (topComponent != null && this.jsonValidIndicator !=null) {
setLayout(new FormLayout("default:grow", "pref, default"));
JPanel jsonMeta = new JPanel(); // name of the scenario element and indicator of
// valid/invalid
jsonMeta.setLayout(new BoxLayout(jsonMeta, BoxLayout.Y_AXIS));
jsonValidIndicator = new JsonValidIndicator();
jsonMeta.add(jsonValidIndicator);
jsonValidIndicator.hide();
jsonMeta.add(this.jsonValidIndicator);
this.jsonValidIndicator.hide();
jsonMeta.add(topComponent);
add(jsonMeta, cc.xy(1, 1));
......
......@@ -96,7 +96,7 @@ public class SettingsDialog extends JDialog {
FormLayout additionalLayout = new FormLayout("5dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 5dlu", // col
"5dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 5dlu"); // rows
FormLayout colorLayout = new FormLayout("5dlu, pref, 2dlu, pref:grow, 2dlu, pref, 2dlu, pref, 5dlu", // col
"5dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 5dlu"); // rows
"5dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 5dlu"); // rows
colorLayeredPane.setLayout(colorLayout);
additionalLayeredPane.setLayout(additionalLayout);
......@@ -237,6 +237,16 @@ public class SettingsDialog extends JDialog {
colorLayeredPane.add(pPedestrianNoTarget, cc.xy(4, 18));
colorLayeredPane.add(bPedestrianNoTarget, cc.xy(6, 18));
// 20 is free
JCheckBox chRandomColors = new JCheckBox(Messages.getString("SettingsDialog.chbUseRandomColors.text"));
chRandomColors.setSelected(model.config.isUseRandomPedestrianColors());
chRandomColors.addItemListener(e -> {
model.config.setUseRandomPedestrianColors(!model.config.isUseRandomPedestrianColors());
model.notifyObservers();
});
colorLayeredPane.add(chRandomColors, cc.xyw(2, 22, 8));
additionalLayeredPane.add(chHideVoronoiDiagram, cc.xyw(2, 2, 5));
additionalLayeredPane.add(chShowObstacles, cc.xyw(2, 4, 5));
......
......@@ -8,6 +8,8 @@ import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
......@@ -220,8 +222,13 @@ public abstract class SimulationRenderer extends DefaultRenderer {
this.agentRender = agentRender;
}
protected Color getPedestrianColor(@NotNull final Agent agent) {
public Color getPedestrianColor(@NotNull final Agent agent) {
int targetId = agent.hasNextTarget() ? agent.getNextTargetId() : -1;
return model.config.getColorByTargetId(targetId).orElseGet(model.config::getPedestrianColor);
if (model.config.isUseRandomPedestrianColors()) {
return model.config.getRandomColor(agent.getId());
}
return model.config.getColorByTargetId(targetId)
.orElseGet(model.config::getPedestrianColor);
}
}
\ No newline at end of file
......@@ -75,19 +75,18 @@ public class PostvisualizationModel extends SimulationModel<PostvisualizationCon
this.pedestrianColorTableModel = new PedestrianColorTableModel();
this.steps = new ArrayList<>();
for (int i = 0; i < 5; i++) {
/*for (int i = 0; i < 5; i++) {
try {
colorEvalFunctions.put(i, new JsonLogicParser("false").parse());
} catch (ParseException e) {
e.printStackTrace();
}
}
}*/
this.pedestrianColorTableModel.addTableModelListener(
e -> {
for (int row = e.getFirstRow(); row <= e.getLastRow(); row++) {
if (row >= 0 && colorEvalFunctions.containsKey(row)
&& e.getColumn() == PedestrianColorTableModel.CIRTERIA_COLUMN) {
if (row >= 0 && e.getColumn() == PedestrianColorTableModel.CIRTERIA_COLUMN) {
try {
VPredicate<JsonNode> evaluator = new JsonLogicParser(
pedestrianColorTableModel.getValueAt(row, e.getColumn()).toString()).parse();
......
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