Commit 7e365e57 authored by Benedikt Zoennchen's avatar Benedikt Zoennchen

change the metric notebook, small changes to the postvis such that other...

change the metric notebook, small changes to the postvis such that other simTimeSteps not equal to 0.4 will be used if they are contained in the postvis.trajectories file.
parent ad2d8105
......@@ -3,11 +3,17 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"code_folding": []
},
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# expand the cell of the notebook\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
......@@ -15,77 +21,314 @@
"import matplotlib.pyplot as plt\n",
"from matplotlib.lines import Line2D\n",
"\n",
"from IPython.core.display import display, HTML\n",
"display(HTML('<style>.container { width:100% !important; }</style>'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Methods to convert Vadere trajectories into a DataFrame"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def fs_append(pedestrianId, fs, llist):\n",
" llist.append([pedestrianId, fs['start']['x'], fs['start']['y'], fs['startTime'], fs['end']['x'], fs['end']['y'], fs['endTime']])\n",
" \n",
"def trajectory_append(pedestrianId, trajectory, llist):\n",
" for fs in trajectory:\n",
" fs_append(pedestrianId, fs, llist)\n",
"\n",
"def trajectories_to_dataframe(trajectories):\n",
" llist = []\n",
" for pedId in trajectories:\n",
" trajectory_append(pedId, trajectories[pedId], llist)\n",
" dataframe = pd.DataFrame(llist, columns=['pedestrianId','startX','startY','startTime','endX','endY','endTime'])\n",
" return dataframe\n",
"\n",
"file = \"./data/trajectories_distance.txt\"\n",
"real_file = \"./data/KO/ko-240-120-240/ko-240-120-240_combined_MB.txt\"\n",
"f = open(file, \"r\")\n",
"header = f.readline();\n",
"trajectories = dict({});\n",
"\n",
"for row in f:\n",
" s = row.split(\" \");\n",
" pedId = int(s[0]);\n",
" footsteps = json.loads(s[1]);\n",
" trajectories[pedId] = footsteps[0]['footSteps'];\n",
" \n",
"def get_trajectory(pedId):\n",
" return trajectories[pedId]\n",
"\n",
"ptrajectories = trajectories_to_dataframe(trajectories)\n",
"ptrajectories.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Helpler method to access parts of the trajectory"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_trajectory(pedId, trajectories):\n",
" \"\"\"returns a data frame containing the trajectory of one specific agent.\"\"\"\n",
" query = 'pedestrianId == ' + str(pedId)\n",
" return trajectories.query(query)\n",
"\n",
"def get_pedestrianIds(trajectories):\n",
" return trajectories['pedestrianId'].unique()\n",
"\n",
"def get_footstep(trajectory, i):\n",
" return trajectory[i];\n",
" \"\"\"returns the i-ths footstep.\"\"\"\n",
" return trajectory.iloc[i];\n",
"\n",
"def start_time(trajectory):\n",
" return trajectory[0]['startTime'];\n",
"def get_footstep_by_time(trajectory, time):\n",
" \"\"\"returns the footstep which happens at time or nothing (None).\"\"\"\n",
" query = 'startTime <= ' + str(time) + ' and ' + str(time) + ' < endTime'\n",
" fs = trajectories.query(query)\n",
" assert len(fs) >= 1\n",
" return fs\n",
"\n",
"def max_start_time(trajectories):\n",
" return max(map(lambda i: start_time(trajectories[i]), trajectories))\n",
"def start_time(trajectory):\n",
" \"\"\"returns the time of the first footstep of the trajectory.\"\"\"\n",
" return get_footstep(trajectory, 0)['startTime'];\n",
"\n",
"def end_time(trajectory):\n",
" return trajectory[-1]['endTime'];\n",
" return get_footstep(trajectory, len(trajectory)-1)['endTime'];\n",
"\n",
"def max_start_time(trajectories):\n",
" \"\"\"returns the time of the first footstep of the trajectory which starts last.\"\"\"\n",
" pedestrianIds = get_pedestrianIds(trajectories)\n",
" return max(map(lambda pedId: start_time(get_trajectory(pedId, trajectories)), pedestrianIds))\n",
"\n",
"def min_end_time(trajectories):\n",
" return min(map(lambda i: end_time(trajectories[i]), trajectories))\n",
"\n",
"def length(fs):\n",
" start = fs['start'];\n",
" end = fs['end'];\n",
" x1 = start['x'];\n",
" y1 = start['y'];\n",
" x2 = end['x'];\n",
" y2 = end['y'];\n",
" \"\"\"returns the time of the last footstep of the trajectory which ends first.\"\"\"\n",
" pedestrianIds = get_pedestrianIds(trajectories)\n",
" return min(map(lambda pedId: end_time(get_trajectory(pedId, trajectories)), pedestrianIds))\n",
"\n",
"def footstep_is_between(fs, time):\n",
" \"\"\"true if the foostep and the intersection with time is not empty.\"\"\"\n",
" startTime = fs['startTime'];\n",
" endTime = fs['endTime'];\n",
" return startTime <= time and time < endTime;\n",
"\n",
"def cut(trajectory, sTime, eTime):\n",
" query = 'startTime >= ' + str(sTime) + ' and endTime < ' + str(eTime)\n",
" return trajectory.query(query)\n",
"\n",
"def cut_soft(trajectory, sTime, eTime):\n",
" query = 'endTime > ' + str(sTime) + ' and startTime < ' + str(eTime)\n",
" return trajectory.query(query)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Helper methods to compute different metrices"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def footstep_length(fs):\n",
" \"\"\"Euclidean length of a footstep.\"\"\"\n",
" x1 = fs['startX'];\n",
" y1 = fs['startY'];\n",
" x2 = fs['endX'];\n",
" y2 = fs['endY'];\n",
" dx = x1-x2;\n",
" dy = y1-y2;\n",
" return np.sqrt(dx*dx + dy*dy);\n",
"\n",
"def trajectory_length(trajectory):\n",
" return sum(map(lambda fs : length(fs), trajectory))\n",
"\n",
"def direction(fs):\n",
" start = fs['start'];\n",
" end = fs['end'];\n",
" x1 = start['x'];\n",
" y1 = start['y'];\n",
" x2 = end['x'];\n",
" y2 = end['y'];\n",
" \"\"\"Euclidean length of a trajectory.\"\"\"\n",
" dx = trajectory['startX']-trajectory['endX']\n",
" dy = trajectory['startY']-trajectory['endY']\n",
" return np.sqrt(dx*dx + dy*dy).sum();\n",
"\n",
"def footstep_direction(fs):\n",
" \"\"\"Vector from start to end position.\"\"\"\n",
" x1 = fs['startX'];\n",
" y1 = fs['startY'];\n",
" x2 = fs['endX'];\n",
" y2 = fs['endY'];\n",
" return np.array([x2-x1, y2-y1]);\n",
"\n",
"def duration(fs):\n",
"def footstep_duration(fs):\n",
" \"\"\"Duration of a footstep.\"\"\"\n",
" startTime = fs['startTime'];\n",
" endTime = fs['endTime'];\n",
" return endTime-startTime;\n",
"\n",
"def speed(fs):\n",
" return length(fs) / duration(fs);\n",
"def trajectory_duration(trajectory):\n",
" \"\"\"Euclidean length of a trajectory.\"\"\"\n",
" return (trajectory['endTime'] - trajectory['startTime']).sum();\n",
"\n",
"def is_between(fs, time):\n",
" startTime = fs['startTime'];\n",
" endTime = fs['endTime'];\n",
" return startTime <= time and time < endTime;\n",
"def footstep_speed(fs):\n",
" \"\"\"Speed of the footstep.\"\"\"\n",
" return footstep_length(fs) / footstep_duration(fs);\n",
"\n",
"def footstep(trajectory, time):\n",
" l = list(filter(lambda fs : is_between(fs, time), trajectory))\n",
" assert len(l) <= 1;\n",
" if len(l) == 1:\n",
" return l[0];\n",
"def trajectory_speed(fs):\n",
" \"\"\"Speed of the trajectory.\"\"\"\n",
" return trajectory_length(fs) / trajectory_duration(fs);\n",
"\n",
"#def trajectory_positions(trajectory, times):\n",
"# mask = trajectory[['startTime', 'endTime']].mask(lambda x: x**2)\n",
"# (df['date'] > '2000-6-1') & (df['date'] <= '2000-6-10')\n",
"# duration = trajectory['endTime'] - trajectory['startTime']\n",
"# dx = trajectory['endX'] - trajectory['startX']\n",
"# dy = trajectory['endY'] - trajectory['startY']\n",
"# direction = \n",
" \n",
"def filter_trajectories(trajectories, times):\n",
" \"\"\"Filters trajectory by times.\"\"\"\n",
" rows = []\n",
" for row in trajectories.itertuples():\n",
" if len(list(filter(lambda b: b, map(lambda t: row.startTime <= t and t < row.endTime, times)))) > 0:\n",
" rows.append(row)\n",
" return pd.DataFrame(rows)\n",
"\n",
"def trajectories_position(trajectories, times):\n",
" \"\"\"Transforms trajectories into positions at each time in times such that each position is computed by linear interpolation.\"\"\"\n",
" rows = []\n",
" #print(trajectories)\n",
" for row in trajectories.itertuples():\n",
" llist = list(filter(lambda t: row.startTime <= t and t < row.endTime, times))\n",
" assert len(llist) == 1 or len(llist) == 0\n",
" if len(llist) > 0:\n",
" time = llist[0]\n",
" dur = row.endTime - row.startTime\n",
" partial_dur = time - row.startTime\n",
" ratio = partial_dur / dur\n",
" direction = np.array([row.endX - row.startX, row.endY - row.startY])\n",
" l = np.linalg.norm(direction)\n",
" if l > 0:\n",
" partial_l = l * ratio;\n",
" v = direction / l * partial_l;\n",
" pos = np.array([row.startX, row.startY]) + v;\n",
" rows.append([row.pedestrianId, pos[0], pos[1], time])\n",
" else:\n",
" rows.append([row.pedestrianId, np.nan, np.nan, time])\n",
" dataframe = pd.DataFrame(rows, columns=['pedestrianId','x','y','time'])\n",
" return dataframe\n",
" \n",
"def euclid_d(trajPos1, trajPos2):\n",
" \"\"\"Computes the total (Euclidean) distance between two trajectories.\n",
" Assumption: trajectories are both cut acccordingly! \n",
" \"\"\"\n",
" assert len(trajPos1) == len(trajPos2)\n",
" dx = trajPos1['x'] - trajPos2['x']\n",
" dy = trajPos1['y'] - trajPos2['y']\n",
" norm = np.sqrt(dx**2 + dy**2)\n",
" return norm.sum() / len(dx)\n",
"\n",
"def euclid_path_length(trajPos1, trajPos2):\n",
" \"\"\"Computes the total (Euclidean) path length difference between two trajectories.\n",
" Assumption: trajectories are both cut acccordingly!\n",
" \"\"\"\n",
" count = len(trajPos1)\n",
" pad = pd.DataFrame([[np.nan, np.nan, np.nan, np.nan]], columns=['pedestrianId','x','y','time'])\n",
" trajPos1Pad = pd.concat([pad, trajPos1], ignore_index=True)\n",
" trajPos2Pad = pd.concat([pad, trajPos1], ignore_index=True)\n",
" dx1 = trajPos1['x'] - trajPos1Pad['x']\n",
" dy1 = trajPos1['y'] - trajPos1Pad['y']\n",
" dx2 = trajPos2['x'] - trajPos2Pad['x']\n",
" dy2 = trajPos2['y'] - trajPos2Pad['y']\n",
" dx = dx1 - dx2\n",
" dy = dy1 - dy2\n",
" diff = np.sqrt(dx**2 + dy**2)\n",
" return diff.sum()\n",
"\n",
"def euclid_len(trajectory, sTime, eTime):\n",
" \"\"\"Computes the total (Euclidean) length of the trajectory in between [sTime;eTime].\"\"\"\n",
" cut_traj = cut_soft(trajectory, sTime, eTime);\n",
" return trajectory_length(cut_traj)\n",
" \n",
"def inter_agent_d(trajPos):\n",
" \"\"\"Computes the inter agent (Euclidean) distance between all pairs of agents.\n",
" Assumption: the trajectory is cut accordingly, ie the time is equal for\n",
" each position.\n",
" \"\"\"\n",
" s = 0\n",
" min_index = min(trajectories.keys())\n",
" c = 0\n",
" llen = len(trajPos)\n",
" for index1, row1 in trajPos.iterrows():\n",
" for index2, row2 in trajPos.tail(llen-1-index1).iterrows():\n",
" x1 = row1['x']\n",
" y1 = row1['y']\n",
" x2 = row2['x']\n",
" y2 = row2['y']\n",
" dx = x1 - x2\n",
" dy = y1 - y2\n",
" s = s + np.sqrt(dx**2 + dy**2)\n",
" c = c + 1\n",
" if c == 0:\n",
" return 0\n",
" else:\n",
" return s / c\n",
" \n",
"def total_inter_agent(trajectories1, trajectories2, times):\n",
" \"\"\"too expensive! TODO!\"\"\"\n",
" return sum(map(lambda t: inter_agent_d(trajectories_position(trajectories1, [t])) - inter_agent_d(trajectories_position(trajectories2, [t])), times)) / len(times)\n",
" \n",
"#start_time(get_trajectory(1, ptrajectories))\n",
"#max_start_time(ptrajectories)\n",
"#end_time(get_trajectory(1, ptrajectories))\n",
"#foot_step_length(get_footstep(get_trajectory(1, ptrajectories), 0))\n",
"#trajectory_length(get_trajectory(1, ptrajectories))\n",
"#trajectory_speed(get_trajectory(1, ptrajectories))\n",
"cutTraj = cut(get_trajectory(1, ptrajectories), 0.0, 10.0)[['startTime', 'endTime']]\n",
"#cutTraj.mask(cutTraj['startTime'] <= 4 and 4 > cutTraj['endTime'])\n",
"#start_time(get_trajectory(1, ptrajectories))\n",
"#trajectories_position(ptrajectories, [1,2,3,4]).head()\n",
"trajPos1 = trajectories_position(get_trajectory(2, ptrajectories), [1,2,3,4,5,6,8,9,10,11,12,13])\n",
"trajPos2 = trajectories_position(get_trajectory(7, ptrajectories), [1,2,3,4,5,6,8,9,10,11,12,13])\n",
"trajPos1 = trajPos1[~np.isnan(trajPos1.x)]\n",
"trajPos2 = trajPos2[~np.isnan(trajPos2.x)]\n",
"euclid_path_length(trajPos1, trajPos2)\n",
"euclid_len(ptrajectories,0,10000)\n",
"#print(total_inter_agent(ptrajectories, ptrajectories, [1,2]))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"trajPos2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"code_folding": []
},
"outputs": [],
"source": [
"def position(trajectory, time):\n",
" fs = footstep(trajectory, time);\n",
" if fs != None:\n",
......@@ -104,12 +347,7 @@
" partial_l = l * ratio;\n",
" v = direction(fs) / l * partial_l;\n",
" return np.array([x1, y1]) + v;\n",
" \n",
"def cut(trajectory, sTime, eTime):\n",
" return list(filter(lambda fs : fs['startTime'] >= sTime and fs['endTime'] < eTime, trajectory))\n",
"\n",
"def cut_soft(trajectory, sTime, eTime):\n",
" return list(filter(lambda fs : fs['startTime'] >= sTime and fs['startTime'] < eTime, trajectory))\n",
"\n",
"def euclid_d(traj1, traj2, times):\n",
" \"\"\"Computes the total (Euclidean) distance between two trajectories at certain times.\"\"\"\n",
......@@ -187,6 +425,7 @@
" \n",
"def load_experiment(file):\n",
" fps = 16\n",
" pad = pd.DataFrame([[np.nan, np.nan, np.nan, np.nan, np.nan]], columns=['pedestrianId', 'timeStep', 'x', 'y', 'e'])\n",
" data = pd.read_csv(\n",
" file, \n",
" sep=' ', \n",
......@@ -194,10 +433,16 @@
" index_col=False, \n",
" header=None, \n",
" skiprows=0)\n",
" \n",
" cc = pd.concat([pad, data], ignore_index=True)\n",
" \n",
" data['x'] = data['x'] / 100\n",
" data['y'] = data['y'] / 100\n",
" data['timeStep'] = data['timeStep'] / fps\n",
" data['endX'] = data['x'] / 100 + 18.7\n",
" data['endY'] = data['y'] / 100 + 4.2\n",
" data['startX'] = cc['x'] / 100 + 18.7\n",
" data['startY'] = cc['y'] / 100 + 4.2\n",
" data['startTime'] = data['timeStep'] / fps - 1/fps\n",
" data['endTime'] = data['timeStep'] / fps\n",
" data = data.drop(columns=['timeStep','x','y','e'])\n",
" return data\n",
" \n",
"def to_trajectories(data):\n",
......@@ -221,9 +466,21 @@
" pedId = data['pedestrianId'][i]\n",
" return trajectories\n",
"\n",
"def to_postVis(df):\n",
" simTimeStep = 0.4\n",
" fps = 16\n",
" df['timeStep'] = np.ceil(df['endTime'] / (1/fps)).astype(np.int)\n",
" df['x'] = df['endX']\n",
" df['y'] = df['endY']\n",
" df['simTime'] = df['endTime']\n",
" df = df.drop(columns=['startX','startY','endX','endY','startTime', 'endTime']) \n",
" return df\n",
"#times = np.linspace(4,10,10)\n",
"#euclid_d(get_trajectory(1), get_trajectory(1), times)\n",
"#to_trajectories(load_experiment(real_file))[1]"
"#to_trajectories(load_experiment(real_file))[1]\n",
"\n",
"file = \"./data/trajectories_distance.txt\"\n",
"real_file = \"./data/KO/ko-240-120-240/ko-240-120-240_combined_MB.txt\""
]
},
{
......@@ -232,7 +489,11 @@
"metadata": {},
"outputs": [],
"source": [
"trajectoriesReal = to_trajectories(load_experiment(real_file));"
"data = load_experiment(real_file)\n",
"#trajectoriesReal = to_trajectories(data)\n",
"data.query('pedestrianId == 1').head()\n",
"to_postVis(data).to_csv('expteriment_2.trajectories',index=False,sep=' ')\n",
"to_postVis(data).head(10)"
]
},
{
......@@ -261,17 +522,15 @@
"fig1 = plt.figure(figsize=(10,10))\n",
"ax1 = fig1.add_subplot(111)\n",
"\n",
"x_rcenter = -1.0\n",
"y_rcenter = 1.0\n",
"\n",
"x_vcenter = 17.5\n",
"y_vcenter = 5.2\n",
"for i in range(len(trajectoriesReal)):\n",
" x, y, line = to_line(trajectoriesReal[i+1], -2)\n",
" x, y, line = to_line(trajectoriesReal[i+1], 14)\n",
" ax1.add_line(line)\n",
" \n",
"ax1.set_xlim(x_rcenter-5, x_rcenter+5)\n",
"ax1.set_ylim(y_rcenter-4, y_rcenter+4)\n",
"ax1.set_xlim(x_vcenter-5, x_vcenter+5)\n",
"ax1.set_ylim(y_vcenter-4, y_vcenter+4)\n",
"ax1.set_aspect(1)\n",
"\n",
"fig2 = plt.figure(figsize=(10,10))\n",
......@@ -325,7 +584,7 @@
"metadata": {},
"outputs": [],
"source": [
"[] is not None"
"pd.DataFrame([[1,2,3,4,5],[6,7,8,9,10]], columns=['pedestrianId','timeStep','x','y','time'])"
]
},
{
......@@ -333,27 +592,35 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"min(trajectories.keys())"
]
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"np.arange(14,15,0.4)"
]
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"range(1, 5)[0]"
]
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
......@@ -361,8 +628,7 @@
"metadata": {},
"outputs": [],
"source": [
"for i in set(range(1,10)):\n",
" print(i)"
"to_postVis(data)"
]
},
{
......@@ -371,7 +637,7 @@
"metadata": {},
"outputs": [],
"source": [
"print(greedy_match(trajectories, trajectories, np.arange(14,15,0.4), euclid_d))"
"1 in [1,2,3]"
]
},
{
......
......@@ -54,10 +54,12 @@ public class TrajectoryReader {
private Set<String> groupIdKeys;
private Set<String> groupSizeKeys;
private Set<String> stridesKeys;
private Set<String> simTimeKeys;
private int pedIdIndex;
private int stepIndex;
private int simTimeIndex;
private int xIndex;
private int yIndex;
private int targetIdIndex;
......@@ -84,12 +86,15 @@ public class TrajectoryReader {
groupIdKeys = new HashSet<>();
groupSizeKeys = new HashSet<>();
stridesKeys = new HashSet<>();
simTimeKeys = new HashSet<>();
//should be set via Processor.getHeader
pedestrianIdKeys.add("id");
pedestrianIdKeys.add("pedestrianId");
stepKeys.add("timeStep");
stepKeys.add("step");
simTimeKeys.add("simTime");
simTimeKeys.add("time");
xKeys.add("x");
yKeys.add("y");
targetIdKeys.add("targetId");
......@@ -100,6 +105,7 @@ public class TrajectoryReader {
pedIdIndex = -1;
stepIndex = -1;
simTimeIndex = -1;
xIndex = -1;
yIndex = -1;
targetIdIndex = -1;
......@@ -146,6 +152,9 @@ public class TrajectoryReader {
else if(stridesKeys.contains(columns[index])) {
stridesIndex = index;
}
else if(simTimeKeys.contains(columns[index])) {
simTimeIndex = index;
}
}
try {
if (pedIdIndex != -1 && xIndex != -1 && yIndex != -1 && stepIndex != -1) {
......@@ -212,6 +221,7 @@ public class TrajectoryReader {
private Pair<Step, Agent> parseRowTokens(@NotNull final String[] rowTokens) {
// time step
int step = Integer.parseInt(rowTokens[stepIndex]);
double simTime = 0.0;
// pedestrian id
int pedestrianId = Integer.parseInt(rowTokens[pedIdIndex]);
......@@ -227,6 +237,10 @@ public class TrajectoryReader {
targets.addFirst(targetId);
ped.setTargets(targets);
if(simTimeIndex != -1) {
simTime = Double.parseDouble(rowTokens[simTimeIndex]);
}
if(groupIdIndex != -1) {
int groupId = Integer.parseInt(rowTokens[groupIdIndex]);
int groupSize = groupSizeIndex != -1 ? Integer.parseInt(rowTokens[groupSizeIndex]) : -1;
......@@ -240,6 +254,6 @@ public class TrajectoryReader {
}
}
return Pair.create(new Step(step), ped);
return simTimeIndex == -1 ? Pair.create(new Step(step), ped) : Pair.create(new Step(step, simTime), ped);
}
}
......@@ -15,6 +15,11 @@ public class Step implements Comparable<Step> {
this.simTimeInSec = null;
}
public Step(final int stepNumber, final double simTimeInSec) {
this.stepNumber = stepNumber;
this.simTimeInSec = simTimeInSec;
}
/**
* Returns an Optional<Double> since the simulation time in seconds may not stored.
*
......
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