Compare commits
No commits in common. "308579279b292c4a227fdbf416d7dc0bb6db3509" and "c238a9eed345f1f0551846425ba84ce6b850efad" have entirely different histories.
308579279b
...
c238a9eed3
@ -1,3 +1,4 @@
|
|||||||
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using OSMDatastructure;
|
using OSMDatastructure;
|
||||||
using OSMDatastructure.Graph;
|
using OSMDatastructure.Graph;
|
||||||
@ -14,37 +15,22 @@ builder.Services.AddSwaggerGen();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapGet("/getRouteBeta", (float latStart, float lonStart, float latEnd, float lonEnd, Tag.SpeedType vehicle, double stayOnSameRoadPriority, double useHigherLevelRoadsPriority, double useRoadsWithLessJunctionsPriority) =>
|
app.MapGet("/getRouteDistance", (float latStart, float lonStart, float latEnd, float lonEnd) =>
|
||||||
{
|
{
|
||||||
DateTime startCalc = DateTime.Now;
|
DateTime startCalc = DateTime.Now;
|
||||||
List<PathNode> result = Pathfinder.AStar("D:/stuttgart-regbez-latest", new Coordinates(latStart, lonStart),
|
List<PathNode> result = Pathfinder.AStarDistance("D:/stuttgart-regbez-latest", new Coordinates(latStart, lonStart), new Coordinates(latEnd, lonEnd));
|
||||||
new Coordinates(latEnd, lonEnd), vehicle, useHigherLevelRoadsPriority, stayOnSameRoadPriority,
|
|
||||||
useRoadsWithLessJunctionsPriority);
|
|
||||||
PathResult pathResult = new PathResult(DateTime.Now - startCalc, result);
|
|
||||||
return RenderPath.Renderer.DrawFromPath(result);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
app.MapGet("/getRoute", (float latStart, float lonStart, float latEnd, float lonEnd, Tag.SpeedType vehicle, double stayOnSameRoadPriority, double useHigherLevelRoadsPriority, double useRoadsWithLessJunctionsPriority) =>
|
|
||||||
{
|
|
||||||
DateTime startCalc = DateTime.Now;
|
|
||||||
List<PathNode> result = Pathfinder.AStar("D:/stuttgart-regbez-latest", new Coordinates(latStart, lonStart),
|
|
||||||
new Coordinates(latEnd, lonEnd), vehicle, useHigherLevelRoadsPriority, stayOnSameRoadPriority,
|
|
||||||
useRoadsWithLessJunctionsPriority);
|
|
||||||
PathResult pathResult = new PathResult(DateTime.Now - startCalc, result);
|
PathResult pathResult = new PathResult(DateTime.Now - startCalc, result);
|
||||||
return pathResult;
|
return pathResult;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.MapGet("/getShortestRoute", (float latStart, float lonStart, float latEnd, float lonEnd) =>
|
app.MapGet("/getRouteTime", (float latStart, float lonStart, float latEnd, float lonEnd, Tag.SpeedType vehicle) =>
|
||||||
{
|
{
|
||||||
DateTime startCalc = DateTime.Now;
|
DateTime startCalc = DateTime.Now;
|
||||||
List<PathNode> result = Pathfinder.AStar("D:/stuttgart-regbez-latest", new Coordinates(latStart, lonStart),
|
List<PathNode> result = Pathfinder.AStarTime("D:/stuttgart-regbez-latest", new Coordinates(latStart, lonStart), new Coordinates(latEnd, lonEnd), vehicle);
|
||||||
new Coordinates(latEnd, lonEnd), Tag.SpeedType.any, 0, 0,
|
|
||||||
0);
|
|
||||||
PathResult pathResult = new PathResult(DateTime.Now - startCalc, result);
|
PathResult pathResult = new PathResult(DateTime.Now - startCalc, result);
|
||||||
return pathResult;
|
return pathResult;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.MapGet("/getClosestNode", (float lat, float lon) =>
|
app.MapGet("/getClosestNode", (float lat, float lon) =>
|
||||||
@ -71,11 +57,18 @@ app.Run();
|
|||||||
internal class PathResult
|
internal class PathResult
|
||||||
{
|
{
|
||||||
[JsonInclude]public TimeSpan calcTime;
|
[JsonInclude]public TimeSpan calcTime;
|
||||||
|
[JsonInclude] public double pathWeight = double.MaxValue;
|
||||||
|
[JsonInclude] public double pathTravelDistance = double.MaxValue;
|
||||||
[JsonInclude]public List<PathNode> pathNodes;
|
[JsonInclude]public List<PathNode> pathNodes;
|
||||||
|
|
||||||
public PathResult(TimeSpan calcTime, List<PathNode> pathNodes)
|
public PathResult(TimeSpan calcTime, List<PathNode> pathNodes)
|
||||||
{
|
{
|
||||||
this.calcTime = calcTime;
|
this.calcTime = calcTime;
|
||||||
this.pathNodes = pathNodes;
|
this.pathNodes = pathNodes;
|
||||||
|
if (pathNodes.Count > 0)
|
||||||
|
{
|
||||||
|
this.pathWeight = pathNodes.Last().currentPathWeight;
|
||||||
|
this.pathTravelDistance = pathNodes.Last().currentPathLength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace OSMDatastructure.Graph;
|
namespace OSMDatastructure.Graph;
|
||||||
@ -7,8 +8,21 @@ public class OsmNode
|
|||||||
{
|
{
|
||||||
public ulong nodeId { get; }
|
public ulong nodeId { get; }
|
||||||
public HashSet<OsmEdge> edges { get; set; }
|
public HashSet<OsmEdge> edges { get; set; }
|
||||||
public Coordinates coordinates { get; }
|
public Coordinates coordinates { get; }
|
||||||
|
|
||||||
|
[JsonIgnore][NonSerialized]public OsmNode? previousPathNode = null;
|
||||||
|
[JsonIgnore][NonSerialized]public double currentPathWeight = double.MaxValue;
|
||||||
|
[JsonIgnore][NonSerialized]public double currentPathLength = double.MaxValue;
|
||||||
|
[JsonIgnore][NonSerialized]public double directDistanceToGoal = double.MaxValue;
|
||||||
|
|
||||||
|
[OnDeserialized]
|
||||||
|
internal void SetDefaultValues(StreamingContext context)
|
||||||
|
{
|
||||||
|
currentPathWeight = double.MaxValue;
|
||||||
|
currentPathLength = double.MaxValue;
|
||||||
|
directDistanceToGoal = double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
public OsmNode(ulong nodeId, float lat, float lon)
|
public OsmNode(ulong nodeId, float lat, float lon)
|
||||||
{
|
{
|
||||||
this.nodeId = nodeId;
|
this.nodeId = nodeId;
|
||||||
@ -39,6 +53,9 @@ public class OsmNode
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{nodeId} {coordinates} ec:{edges.Count}";
|
if(previousPathNode is not null)
|
||||||
|
return $"{nodeId} {coordinates} ec:{edges.Count} d:{directDistanceToGoal} w:{currentPathWeight} l:{currentPathLength} p:{previousPathNode.nodeId}";
|
||||||
|
return
|
||||||
|
$"{nodeId} {coordinates} ec:{edges.Count} d:{directDistanceToGoal} w:{currentPathWeight} l:{currentPathLength} null";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -30,10 +30,6 @@ public class Tag
|
|||||||
case TagType.id:
|
case TagType.id:
|
||||||
this.value = value.GetUInt64();
|
this.value = value.GetUInt64();
|
||||||
break;
|
break;
|
||||||
case TagType.name:
|
|
||||||
case TagType.tagref:
|
|
||||||
this.value = value.GetString();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
this.value = value;
|
this.value = value;
|
||||||
break;
|
break;
|
||||||
@ -45,58 +41,49 @@ public class Tag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashSet<Tag> ConvertToTags(string key, string value)
|
public static Tag? ConvertToTag(string key, string value)
|
||||||
{
|
{
|
||||||
HashSet<Tag> ret = new HashSet<Tag>();
|
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case "highway":
|
case "highway":
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ret.Add(new Tag(TagType.highway, (WayType)Enum.Parse(typeof(WayType), value, true)));
|
return new Tag(TagType.highway, (WayType)Enum.Parse(typeof(WayType), value, true));
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
ret.Add(new Tag(TagType.highway, WayType.unclassified));
|
return new Tag(TagType.highway, WayType.unclassified);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case "maxspeed":
|
case "maxspeed":
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
byte speed = Convert.ToByte(value);
|
byte speed = Convert.ToByte(value);
|
||||||
if (speed != 255)
|
if (speed == 255)
|
||||||
ret.Add(new Tag(TagType.maxspeed, speed));
|
return new Tag(TagType.highway, false);
|
||||||
|
else
|
||||||
|
return new Tag(TagType.maxspeed, speed);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
ret.Add(new Tag(TagType.maxspeed, byte.MinValue));
|
//Console.WriteLine(e);
|
||||||
|
//Console.WriteLine("Continuing...");
|
||||||
|
return new Tag(TagType.maxspeed, byte.MaxValue);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case "oneway":
|
case "oneway":
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
case "yes":
|
case "yes":
|
||||||
ret.Add(new Tag(TagType.oneway, true));
|
return new Tag(TagType.oneway, true);
|
||||||
break;
|
|
||||||
case "-1":
|
case "-1":
|
||||||
ret.Add(new Tag(TagType.forward, false));
|
return new Tag(TagType.forward, false);
|
||||||
ret.Add(new Tag(TagType.oneway, true));
|
|
||||||
break;
|
|
||||||
case "no":
|
case "no":
|
||||||
ret.Add(new Tag(TagType.oneway, false));
|
return new Tag(TagType.oneway, false);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
case "name":
|
|
||||||
ret.Add(new Tag(TagType.name, value));
|
|
||||||
break;
|
|
||||||
case "ref":
|
|
||||||
ret.Add(new Tag(TagType.tagref, value));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
@ -106,7 +93,7 @@ public class Tag
|
|||||||
|
|
||||||
public enum TagType : byte
|
public enum TagType : byte
|
||||||
{
|
{
|
||||||
highway, oneway, footway, sidewalk, cycleway, busway, forward, maxspeed, name, surface, lanes, access, tracktype, id, tagref
|
highway, oneway, footway, sidewalk, cycleway, busway, forward, maxspeed, name, surface, lanes, access, tracktype, id
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly Dictionary<WayType, byte> defaultSpeedCar = new() {
|
public static readonly Dictionary<WayType, byte> defaultSpeedCar = new() {
|
||||||
@ -117,8 +104,8 @@ public class Tag
|
|||||||
{ WayType.primary, 65 },
|
{ WayType.primary, 65 },
|
||||||
{ WayType.secondary, 60 },
|
{ WayType.secondary, 60 },
|
||||||
{ WayType.tertiary, 50 },
|
{ WayType.tertiary, 50 },
|
||||||
{ WayType.unclassified, 30 },
|
{ WayType.unclassified, 15 },
|
||||||
{ WayType.residential, 15 },
|
{ WayType.residential, 10 },
|
||||||
{ WayType.motorway_link, 60 },
|
{ WayType.motorway_link, 60 },
|
||||||
{ WayType.trunk_link, 50 },
|
{ WayType.trunk_link, 50 },
|
||||||
{ WayType.primary_link, 50 },
|
{ WayType.primary_link, 50 },
|
||||||
@ -127,7 +114,7 @@ public class Tag
|
|||||||
{ WayType.living_street, 10 },
|
{ WayType.living_street, 10 },
|
||||||
{ WayType.service, 1 },
|
{ WayType.service, 1 },
|
||||||
{ WayType.pedestrian, 0 },
|
{ WayType.pedestrian, 0 },
|
||||||
{ WayType.track, 1 },
|
{ WayType.track, 15 },
|
||||||
{ WayType.bus_guideway, 0 },
|
{ WayType.bus_guideway, 0 },
|
||||||
{ WayType.escape, 0 },
|
{ WayType.escape, 0 },
|
||||||
{ WayType.raceway, 0 },
|
{ WayType.raceway, 0 },
|
||||||
|
@ -25,10 +25,9 @@ public class TagManager
|
|||||||
|
|
||||||
public void AddTag(ulong wayId, string key, string value)
|
public void AddTag(ulong wayId, string key, string value)
|
||||||
{
|
{
|
||||||
HashSet<Tag> pTags = Tag.ConvertToTags(key, value);
|
Tag? tag = Tag.ConvertToTag(key, value);
|
||||||
if(pTags.Count > 0)
|
if(tag is not null)
|
||||||
foreach (Tag pTag in pTags)
|
AddTag(wayId, tag);
|
||||||
AddTag(wayId, pTag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddTag(ulong wayId, Tag tag)
|
public void AddTag(ulong wayId, Tag tag)
|
||||||
|
@ -6,6 +6,12 @@ namespace Pathfinding;
|
|||||||
|
|
||||||
public class PathNode : OsmNode
|
public class PathNode : OsmNode
|
||||||
{
|
{
|
||||||
|
[JsonInclude]public new double directDistanceToGoal = double.MaxValue;
|
||||||
|
[JsonInclude]public double directDistanceDelta = double.MaxValue;
|
||||||
|
[JsonInclude]public new double currentPathLength = double.MaxValue;
|
||||||
|
[JsonInclude]public double pathDistanceDelta = double.MaxValue;
|
||||||
|
[JsonInclude]public new double currentPathWeight = double.MaxValue;
|
||||||
|
[JsonInclude]public double pathWeightDelta = double.MaxValue;
|
||||||
[JsonInclude]public Dictionary<string, string> tags = new();
|
[JsonInclude]public Dictionary<string, string> tags = new();
|
||||||
|
|
||||||
public PathNode(ulong nodeId, float lat, float lon) : base(nodeId, lat, lon)
|
public PathNode(ulong nodeId, float lat, float lon) : base(nodeId, lat, lon)
|
||||||
@ -16,11 +22,19 @@ public class PathNode : OsmNode
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PathNode? FromOsmNode(OsmNode? node, HashSet<Tag>? tags)
|
public static PathNode? FromOsmNode(OsmNode? node, HashSet<Tag>? tags, double pathDistanceDelta, double pathWeightDelta, double directDistanceDelta)
|
||||||
{
|
{
|
||||||
if (node is null)
|
if (node is null)
|
||||||
return null;
|
return null;
|
||||||
PathNode retNode = new(node.nodeId, node.coordinates);
|
PathNode retNode = new(node.nodeId, node.coordinates)
|
||||||
|
{
|
||||||
|
currentPathLength = node.currentPathLength,
|
||||||
|
currentPathWeight = double.IsPositiveInfinity(node.currentPathWeight) ? double.MaxValue : node.currentPathWeight,
|
||||||
|
directDistanceToGoal = node.directDistanceToGoal,
|
||||||
|
directDistanceDelta = directDistanceDelta,
|
||||||
|
pathDistanceDelta = pathDistanceDelta,
|
||||||
|
pathWeightDelta = pathWeightDelta
|
||||||
|
};
|
||||||
if (tags != null)
|
if (tags != null)
|
||||||
foreach (Tag tag in tags)
|
foreach (Tag tag in tags)
|
||||||
{
|
{
|
||||||
|
@ -1,129 +1,78 @@
|
|||||||
using OSMDatastructure;
|
using OSMDatastructure;
|
||||||
using OSMDatastructure.Graph;
|
using OSMDatastructure.Graph;
|
||||||
using static OSMDatastructure.Tag;
|
|
||||||
using WayType = OSMDatastructure.Tag.WayType;
|
using WayType = OSMDatastructure.Tag.WayType;
|
||||||
|
|
||||||
namespace Pathfinding;
|
namespace Pathfinding;
|
||||||
|
|
||||||
public static class Pathfinder
|
public static partial class Pathfinder
|
||||||
{
|
{
|
||||||
|
|
||||||
public static List<PathNode> AStar(string workingDir, Coordinates startCoordinates, Coordinates goalCoordinates,
|
private static ValueTuple<OsmNode?, OsmNode?> SetupNodes(Coordinates startCoordinates, Coordinates goalCoordinates, RegionManager regionManager )
|
||||||
SpeedType vehicle, double heuristicRoadLevelPriority, double heuristicSameRoadPriority,
|
|
||||||
double heuristicFewJunctionsPriority)
|
|
||||||
{
|
{
|
||||||
RegionManager regionManager = new RegionManager(workingDir);
|
ValueTuple<OsmNode?, OsmNode?> retTuple = new();
|
||||||
OsmNode? startNode = regionManager.ClosestNodeToCoordinates(startCoordinates, vehicle);
|
retTuple.Item1 = regionManager.ClosestNodeToCoordinates(startCoordinates, Tag.SpeedType.any);
|
||||||
OsmNode? goalNode = regionManager.ClosestNodeToCoordinates(goalCoordinates, vehicle);
|
retTuple.Item2 = regionManager.ClosestNodeToCoordinates(goalCoordinates, Tag.SpeedType.any);
|
||||||
if (startNode is null || goalNode is null)
|
if (retTuple.Item1 is null || retTuple.Item2 is null)
|
||||||
return new List<PathNode>();
|
return retTuple;
|
||||||
|
retTuple.Item1.currentPathWeight = 0;
|
||||||
PriorityQueue<OsmNode, double> openSetfScore = new();
|
retTuple.Item1.currentPathLength = 0;
|
||||||
openSetfScore.Enqueue(startNode, 0);
|
retTuple.Item1.directDistanceToGoal = Utils.DistanceBetween(retTuple.Item1, retTuple.Item2);
|
||||||
Dictionary<OsmNode, OsmNode> cameFromDict = new();
|
return retTuple;
|
||||||
Dictionary<OsmNode, double> gScore = new();
|
|
||||||
gScore.Add(startNode, 0);
|
|
||||||
|
|
||||||
while (openSetfScore.Count > 0)
|
|
||||||
{
|
|
||||||
OsmNode currentNode = openSetfScore.Dequeue();
|
|
||||||
if (currentNode.Equals(goalNode))
|
|
||||||
return GetPath(cameFromDict, goalNode, regionManager);
|
|
||||||
|
|
||||||
foreach (OsmEdge edge in currentNode.edges)
|
|
||||||
{
|
|
||||||
OsmNode? neighbor = regionManager.GetNode(edge.neighborId, edge.neighborRegion);
|
|
||||||
if (neighbor is not null)
|
|
||||||
{
|
|
||||||
double tentativeGScore = gScore[currentNode] + Weight(currentNode, neighbor, edge, vehicle, regionManager);
|
|
||||||
gScore.TryAdd(neighbor, double.MaxValue);
|
|
||||||
if (tentativeGScore < gScore[neighbor])
|
|
||||||
{
|
|
||||||
if (cameFromDict.ContainsKey(neighbor))
|
|
||||||
cameFromDict[neighbor] = currentNode;
|
|
||||||
else
|
|
||||||
cameFromDict.Add(neighbor, currentNode);
|
|
||||||
if (gScore.ContainsKey(neighbor))
|
|
||||||
gScore[neighbor] = tentativeGScore;
|
|
||||||
else
|
|
||||||
gScore.Add(neighbor, tentativeGScore);
|
|
||||||
double h = Heuristic(currentNode, neighbor, goalNode, edge, vehicle, regionManager,
|
|
||||||
heuristicRoadLevelPriority, heuristicFewJunctionsPriority, heuristicSameRoadPriority);
|
|
||||||
//Console.WriteLine($"Queue: {openSetfScore.Count:00000} Current Distance: {Utils.DistanceBetween(currentNode, goalNode):000000.00} Visited: {cameFromDict.Count:00000} Current heuristic: {h:00000.00}");
|
|
||||||
openSetfScore.Enqueue(neighbor, tentativeGScore + h);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<PathNode>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<PathNode> GetPath(Dictionary<OsmNode, OsmNode> cameFromDict, OsmNode goalNode, RegionManager regionManager)
|
private static double EdgeWeight(OsmNode node1, OsmEdge edge, Tag.SpeedType vehicle, RegionManager regionManager)
|
||||||
{
|
{
|
||||||
List<PathNode> path = new List<PathNode>();
|
OsmNode? node2 = regionManager.GetNode(edge.neighborId, edge.neighborRegion);
|
||||||
OsmNode currentNode = goalNode;
|
if (node2 is null)
|
||||||
while (cameFromDict.ContainsKey(cameFromDict[currentNode]))
|
return double.MaxValue;
|
||||||
{
|
double distance = Utils.DistanceBetween(node1, node2);
|
||||||
OsmEdge? currentEdge = cameFromDict[currentNode].edges.First(edge => edge.neighborId == currentNode.nodeId);
|
byte speed = regionManager.GetSpeedForEdge(node1, edge.wayId, vehicle);
|
||||||
HashSet<Tag>? tags =
|
return speed is 0 ? double.MaxValue : distance / speed;
|
||||||
regionManager.GetRegion(currentNode.coordinates)!.tagManager.GetTagsForWayId(currentEdge.wayId);
|
|
||||||
PathNode? newNode = PathNode.FromOsmNode(currentNode, tags);
|
|
||||||
if(newNode is not null)
|
|
||||||
path.Add(newNode);
|
|
||||||
currentNode = cameFromDict[currentNode];
|
|
||||||
}
|
|
||||||
|
|
||||||
path.Reverse();
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double Weight(OsmNode fromNode, OsmNode neighborNode, OsmEdge edge, SpeedType vehicle, RegionManager regionManager)
|
private static double GetPriority(OsmNode current, OsmNode? previous, OsmEdge edge, Tag.SpeedType vehicle, RegionManager regionManager)
|
||||||
{
|
{
|
||||||
double distance = Utils.DistanceBetween(fromNode, neighborNode);
|
if (vehicle == Tag.SpeedType.any)
|
||||||
double speed = regionManager.GetSpeedForEdge(fromNode, edge.wayId, vehicle);
|
|
||||||
double prio = GetPriorityVehicleRoad(edge, vehicle, regionManager.GetRegion(fromNode.coordinates)!);
|
|
||||||
return distance / (speed + (prio * 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double Heuristic(OsmNode fromNode, OsmNode neighborNode, OsmNode goalNode, OsmEdge edge, SpeedType vehicle, RegionManager regionManager, double roadPriorityFactor, double junctionFactor, double sameRoadFactor)
|
|
||||||
{
|
|
||||||
double roadPriority = GetPriorityVehicleRoad(edge, vehicle, regionManager.GetRegion(fromNode.coordinates)!) * roadPriorityFactor;
|
|
||||||
|
|
||||||
TagManager curTags = regionManager.GetRegion(fromNode.coordinates)!.tagManager;
|
|
||||||
TagManager nextTags = regionManager.GetRegion(neighborNode.coordinates)!.tagManager;
|
|
||||||
|
|
||||||
bool sameName = false;
|
|
||||||
string? curName = (string?)curTags.GetTag(edge.wayId, TagType.name);
|
|
||||||
bool sameRef = false;
|
|
||||||
string? curRef = (string?)curTags.GetTag(edge.wayId, TagType.tagref);
|
|
||||||
if(curName is not null)
|
|
||||||
foreach (OsmEdge pEdge in neighborNode.edges)
|
|
||||||
{
|
|
||||||
if ((string?)nextTags.GetTag(pEdge.wayId, TagType.name) == curName)
|
|
||||||
sameName = true;
|
|
||||||
if ((string?)nextTags.GetTag(pEdge.wayId, TagType.tagref) == curRef)
|
|
||||||
sameRef = true;
|
|
||||||
}
|
|
||||||
double sameRoadName = (sameRef || sameName ? 1 : 0) * sameRoadFactor;
|
|
||||||
|
|
||||||
double junctionCount = (neighborNode.edges.Count > 2 ? 0 : 1) * junctionFactor;
|
|
||||||
|
|
||||||
//Console.WriteLine($"{roadPriority:000.00} {sameRoadName:000.00} {junctionCount:000.00} {distanceImprovement:+000.00;-000.00;0000.00}");
|
|
||||||
|
|
||||||
return Utils.DistanceBetween(neighborNode, goalNode) - (roadPriority + sameRoadName + junctionCount) * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double GetPriorityVehicleRoad(OsmEdge edge, SpeedType vehicle, Region region)
|
|
||||||
{
|
|
||||||
if (vehicle == SpeedType.any)
|
|
||||||
return 1;
|
return 1;
|
||||||
WayType? wayType = (WayType?)region.tagManager.GetTag(edge.wayId, TagType.highway);
|
const double roadPriorityFactor = 1;
|
||||||
|
const double junctionFactor = 2;
|
||||||
|
const double wayChangeFactor = 2;
|
||||||
|
Region r = regionManager.GetRegion(current.coordinates)!;
|
||||||
|
|
||||||
|
double roadPriority = GetPriorityVehicleRoad(edge, vehicle, r) * 0.1 * roadPriorityFactor;
|
||||||
|
double roadSpeed = regionManager.GetSpeedForEdge(current, edge.wayId, vehicle);
|
||||||
|
|
||||||
|
if(vehicle == Tag.SpeedType.pedestrian)
|
||||||
|
return (current.directDistanceToGoal / roadSpeed) * roadPriority;
|
||||||
|
|
||||||
|
ulong previousWayId = UInt64.MaxValue;
|
||||||
|
if (previous?.edges is not null)
|
||||||
|
{
|
||||||
|
foreach (OsmEdge e in previous.edges)
|
||||||
|
if (e.neighborId.Equals(current.nodeId))
|
||||||
|
{
|
||||||
|
previousWayId = e.wayId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double junctionPriority = (current.edges.Count > 2 ? 1 : 0) * junctionFactor;
|
||||||
|
double wayChange = (previousWayId != edge.wayId ? 1 : 0) * wayChangeFactor;
|
||||||
|
|
||||||
|
if(vehicle == Tag.SpeedType.car)
|
||||||
|
return (current.directDistanceToGoal / roadSpeed) * roadPriority * ((junctionPriority + wayChange) / 2);
|
||||||
|
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetPriorityVehicleRoad(OsmEdge edge, Tag.SpeedType vehicle, Region region)
|
||||||
|
{
|
||||||
|
if (vehicle == Tag.SpeedType.any)
|
||||||
|
return 1;
|
||||||
|
WayType? wayType = (WayType?)region.tagManager.GetTag(edge.wayId, Tag.TagType.highway);
|
||||||
if(wayType is null)
|
if(wayType is null)
|
||||||
return 0;
|
return double.MaxValue;
|
||||||
if (vehicle == SpeedType.car)
|
if (vehicle == Tag.SpeedType.car)
|
||||||
{
|
{
|
||||||
switch (wayType)
|
switch (wayType)
|
||||||
{
|
{
|
||||||
@ -134,24 +83,26 @@ public static class Pathfinder
|
|||||||
case WayType.trunk_link:
|
case WayType.trunk_link:
|
||||||
case WayType.primary:
|
case WayType.primary:
|
||||||
case WayType.primary_link:
|
case WayType.primary_link:
|
||||||
return 8;
|
return 1;
|
||||||
case WayType.secondary:
|
case WayType.secondary:
|
||||||
case WayType.secondary_link:
|
case WayType.secondary_link:
|
||||||
return 6;
|
return 2;
|
||||||
case WayType.tertiary:
|
case WayType.tertiary:
|
||||||
case WayType.tertiary_link:
|
case WayType.tertiary_link:
|
||||||
return 5;
|
return 3;
|
||||||
case WayType.unclassified:
|
case WayType.unclassified:
|
||||||
case WayType.residential:
|
case WayType.residential:
|
||||||
case WayType.road:
|
case WayType.road:
|
||||||
|
return 4;
|
||||||
case WayType.living_street:
|
case WayType.living_street:
|
||||||
return 2;
|
|
||||||
case WayType.service:
|
case WayType.service:
|
||||||
case WayType.track:
|
case WayType.track:
|
||||||
return 0.01;
|
return 5;
|
||||||
|
default:
|
||||||
|
return 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vehicle == SpeedType.pedestrian)
|
if (vehicle == Tag.SpeedType.pedestrian)
|
||||||
{
|
{
|
||||||
switch (wayType)
|
switch (wayType)
|
||||||
{
|
{
|
||||||
@ -162,21 +113,54 @@ public static class Pathfinder
|
|||||||
case WayType.steps:
|
case WayType.steps:
|
||||||
case WayType.residential:
|
case WayType.residential:
|
||||||
case WayType.living_street:
|
case WayType.living_street:
|
||||||
return 10;
|
return 1;
|
||||||
case WayType.service:
|
case WayType.service:
|
||||||
case WayType.cycleway:
|
case WayType.cycleway:
|
||||||
case WayType.bridleway:
|
case WayType.bridleway:
|
||||||
case WayType.road:
|
case WayType.road:
|
||||||
case WayType.track:
|
case WayType.track:
|
||||||
case WayType.unclassified:
|
case WayType.unclassified:
|
||||||
return 5;
|
return 2;
|
||||||
case WayType.tertiary:
|
case WayType.tertiary:
|
||||||
case WayType.tertiary_link:
|
case WayType.tertiary_link:
|
||||||
case WayType.escape:
|
case WayType.escape:
|
||||||
return 2;
|
return 5;
|
||||||
|
default:
|
||||||
|
return 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.01;
|
return 100;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PathNode> GetRouteFromCalc(OsmNode goalNode, RegionManager regionManager)
|
||||||
|
{
|
||||||
|
List<PathNode> path = new();
|
||||||
|
OsmNode? currentNode = goalNode;
|
||||||
|
while (currentNode is not null)
|
||||||
|
{
|
||||||
|
HashSet<Tag>? tags = null;
|
||||||
|
double pathDistanceDelta = 0;
|
||||||
|
double pathWeightDelta = 0;
|
||||||
|
double directDistanceDelta = 0;
|
||||||
|
if (currentNode.previousPathNode is not null)
|
||||||
|
{
|
||||||
|
OsmEdge edge = currentNode.previousPathNode!.edges.First(e => e.neighborId.Equals(currentNode.nodeId));
|
||||||
|
tags = regionManager.GetRegion(currentNode.coordinates)!.tagManager.GetTagsForWayId(edge.wayId);
|
||||||
|
pathDistanceDelta = currentNode.currentPathLength - currentNode.previousPathNode.currentPathLength;
|
||||||
|
pathWeightDelta = currentNode.currentPathWeight - currentNode.previousPathNode.currentPathWeight;
|
||||||
|
directDistanceDelta =
|
||||||
|
currentNode.directDistanceToGoal - currentNode.previousPathNode.directDistanceToGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
PathNode? pn = PathNode.FromOsmNode(currentNode, tags, pathDistanceDelta, pathWeightDelta, directDistanceDelta);
|
||||||
|
if(pn is not null)
|
||||||
|
path.Add(pn!);
|
||||||
|
currentNode = currentNode.previousPathNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.Reverse();
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
51
Pathfinding/Pathfinder_Distance.cs
Normal file
51
Pathfinding/Pathfinder_Distance.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using OSMDatastructure;
|
||||||
|
using OSMDatastructure.Graph;
|
||||||
|
|
||||||
|
namespace Pathfinding;
|
||||||
|
|
||||||
|
public static partial class Pathfinder
|
||||||
|
{
|
||||||
|
public static List<PathNode> AStarDistance(string workingDir, Coordinates start,
|
||||||
|
Coordinates goal)
|
||||||
|
{
|
||||||
|
RegionManager regionManager = new (workingDir);
|
||||||
|
ValueTuple<OsmNode?, OsmNode?> startAndEndNode = SetupNodes(start, goal, regionManager);
|
||||||
|
if (startAndEndNode.Item1 is null || startAndEndNode.Item2 is null)
|
||||||
|
return new List<PathNode>();
|
||||||
|
OsmNode goalNode = startAndEndNode.Item2!;
|
||||||
|
|
||||||
|
PriorityQueue<OsmNode, double> toVisit = new();
|
||||||
|
toVisit.Enqueue(startAndEndNode.Item1, 0);
|
||||||
|
bool stop = false;
|
||||||
|
|
||||||
|
while (toVisit.Count > 0)
|
||||||
|
{
|
||||||
|
OsmNode currentNode = toVisit.Dequeue();
|
||||||
|
|
||||||
|
foreach (OsmEdge edge in currentNode.edges)
|
||||||
|
{
|
||||||
|
OsmNode? neighbor = regionManager.GetNode(edge.neighborId, edge.neighborRegion);
|
||||||
|
if (neighbor is not null)
|
||||||
|
{
|
||||||
|
if (Math.Abs(neighbor.directDistanceToGoal - double.MaxValue) < 1)
|
||||||
|
neighbor.directDistanceToGoal = Utils.DistanceBetween(neighbor, goalNode);
|
||||||
|
double newPotentialLength = currentNode.currentPathLength + Utils.DistanceBetween(currentNode, neighbor);
|
||||||
|
if (newPotentialLength < neighbor.currentPathLength)
|
||||||
|
{
|
||||||
|
neighbor.previousPathNode = currentNode;
|
||||||
|
neighbor.currentPathLength = newPotentialLength;
|
||||||
|
|
||||||
|
if(neighbor.Equals(goalNode))
|
||||||
|
return GetRouteFromCalc(goalNode, regionManager);
|
||||||
|
//stop = true;
|
||||||
|
if (!toVisit.UnorderedItems.Any(item => item.Element.Equals(neighbor)) && !stop)
|
||||||
|
{
|
||||||
|
toVisit.Enqueue(neighbor, neighbor.directDistanceToGoal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GetRouteFromCalc(goalNode, regionManager);
|
||||||
|
}
|
||||||
|
}
|
65
Pathfinding/Pathfinder_Time.cs
Normal file
65
Pathfinding/Pathfinder_Time.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using OSMDatastructure;
|
||||||
|
using OSMDatastructure.Graph;
|
||||||
|
using Utils = OSMDatastructure.Utils;
|
||||||
|
|
||||||
|
namespace Pathfinding;
|
||||||
|
|
||||||
|
public static partial class Pathfinder
|
||||||
|
{
|
||||||
|
public static List<PathNode> AStarTime(string workingDir, Coordinates start,
|
||||||
|
Coordinates goal, Tag.SpeedType vehicle)
|
||||||
|
{
|
||||||
|
RegionManager regionManager = new (workingDir);
|
||||||
|
ValueTuple<OsmNode?, OsmNode?> startAndEndNode = SetupNodes(start, goal, regionManager);
|
||||||
|
if (startAndEndNode.Item1 is null || startAndEndNode.Item2 is null)
|
||||||
|
return new List<PathNode>();
|
||||||
|
OsmNode goalNode = startAndEndNode.Item2!;
|
||||||
|
|
||||||
|
PriorityQueue<OsmNode, double> toVisit = new();
|
||||||
|
toVisit.Enqueue(startAndEndNode.Item1, 0);
|
||||||
|
bool stop = false;
|
||||||
|
|
||||||
|
while (toVisit.Count > 0)
|
||||||
|
{
|
||||||
|
OsmNode currentNode = toVisit.Dequeue();
|
||||||
|
|
||||||
|
foreach (OsmEdge edge in currentNode.edges.Where(
|
||||||
|
edge => regionManager.TestValidConnectionForType(currentNode, edge, vehicle)))
|
||||||
|
{
|
||||||
|
OsmNode? neighbor = regionManager.GetNode(edge.neighborId, edge.neighborRegion);
|
||||||
|
if (neighbor is not null)
|
||||||
|
{
|
||||||
|
if (Math.Abs(neighbor.directDistanceToGoal - double.MaxValue) < 1)
|
||||||
|
neighbor.directDistanceToGoal = Utils.DistanceBetween(neighbor, goalNode);
|
||||||
|
|
||||||
|
double newPotentialWeight = currentNode.currentPathWeight +
|
||||||
|
EdgeWeight(currentNode, edge, vehicle, regionManager);
|
||||||
|
|
||||||
|
if (newPotentialWeight < neighbor.currentPathWeight)
|
||||||
|
{
|
||||||
|
neighbor.previousPathNode = currentNode;
|
||||||
|
neighbor.currentPathWeight = newPotentialWeight;
|
||||||
|
|
||||||
|
if (neighbor.Equals(goalNode))
|
||||||
|
{
|
||||||
|
currentNode = neighbor;
|
||||||
|
currentNode.currentPathLength = 0;
|
||||||
|
while (currentNode != startAndEndNode.Item1 && currentNode.previousPathNode is not null)
|
||||||
|
{
|
||||||
|
currentNode.previousPathNode.currentPathLength = currentNode.currentPathLength +
|
||||||
|
Utils.DistanceBetween(currentNode, currentNode.previousPathNode);
|
||||||
|
currentNode = currentNode.previousPathNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetRouteFromCalc(goalNode, regionManager);
|
||||||
|
}
|
||||||
|
//stop = true;
|
||||||
|
if (!toVisit.UnorderedItems.Any(item => item.Element.Equals(neighbor)))
|
||||||
|
toVisit.Enqueue(neighbor, GetPriority(currentNode, currentNode.previousPathNode, edge, vehicle, regionManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GetRouteFromCalc(goalNode, regionManager);
|
||||||
|
}
|
||||||
|
}
|
@ -95,13 +95,14 @@ namespace Pathfinding
|
|||||||
hasConnectionUsingVehicle = false;
|
hasConnectionUsingVehicle = false;
|
||||||
foreach (OsmEdge edge in node.edges)
|
foreach (OsmEdge edge in node.edges)
|
||||||
{
|
{
|
||||||
if (TestValidConnectionForType(node, edge, vehicle))
|
byte speed = GetSpeedForEdge(node, edge.wayId, vehicle);
|
||||||
|
if (speed != 0)
|
||||||
hasConnectionUsingVehicle = true;
|
hasConnectionUsingVehicle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double nodeDistance = Utils.DistanceBetween(node, coordinates);
|
double nodeDistance = Utils.DistanceBetween(node, coordinates);
|
||||||
if (hasConnectionUsingVehicle && nodeDistance < distance)
|
if (nodeDistance < distance && hasConnectionUsingVehicle)
|
||||||
{
|
{
|
||||||
closest = node;
|
closest = node;
|
||||||
distance = nodeDistance;
|
distance = nodeDistance;
|
||||||
@ -120,7 +121,7 @@ namespace Pathfinding
|
|||||||
{
|
{
|
||||||
case Tag.SpeedType.pedestrian:
|
case Tag.SpeedType.pedestrian:
|
||||||
speed = Tag.defaultSpeedPedestrian[wayType];
|
speed = Tag.defaultSpeedPedestrian[wayType];
|
||||||
return speed;
|
return speed is not 0 ? speed : (byte)0;
|
||||||
case Tag.SpeedType.car:
|
case Tag.SpeedType.car:
|
||||||
byte? maxSpeed = (byte?)tags.GetTag(wayId, Tag.TagType.maxspeed);
|
byte? maxSpeed = (byte?)tags.GetTag(wayId, Tag.TagType.maxspeed);
|
||||||
speed = Tag.defaultSpeedCar[wayType];
|
speed = Tag.defaultSpeedCar[wayType];
|
||||||
|
@ -129,10 +129,9 @@ public class RegionConverter
|
|||||||
currentTags.TryAdd(Tag.TagType.id, Convert.ToUInt64(wayReader.GetAttribute("id")!));
|
currentTags.TryAdd(Tag.TagType.id, Convert.ToUInt64(wayReader.GetAttribute("id")!));
|
||||||
if (wayReader.Name == "tag")
|
if (wayReader.Name == "tag")
|
||||||
{
|
{
|
||||||
HashSet<Tag> pTags = Tag.ConvertToTags(wayReader.GetAttribute("k")!, wayReader.GetAttribute("v")!);
|
Tag? wayTag = Tag.ConvertToTag(wayReader.GetAttribute("k")!, wayReader.GetAttribute("v")!);
|
||||||
if(pTags.Count > 0)
|
if(wayTag is not null)
|
||||||
foreach (Tag pTag in pTags)
|
currentTags.TryAdd(wayTag.key, wayTag.value);
|
||||||
currentTags.TryAdd(pTag.key, pTag.value);
|
|
||||||
}
|
}
|
||||||
else if (wayReader.Name == "nd")
|
else if (wayReader.Name == "nd")
|
||||||
{
|
{
|
||||||
@ -147,42 +146,31 @@ public class RegionConverter
|
|||||||
{
|
{
|
||||||
ulong node1Id = currentNodeIds[i];
|
ulong node1Id = currentNodeIds[i];
|
||||||
ulong node2Id = currentNodeIds[i+1];
|
ulong node2Id = currentNodeIds[i+1];
|
||||||
if (nodeRegions.ContainsKey(node1Id) && nodeRegions.ContainsKey(node2Id))
|
if (currentTags.ContainsKey(Tag.TagType.oneway) && (bool)currentTags[Tag.TagType.oneway] && nodeRegions.ContainsKey(node1Id) && nodeRegions.ContainsKey(node2Id))
|
||||||
{
|
{
|
||||||
if (currentTags.ContainsKey(Tag.TagType.oneway) && (bool)currentTags[Tag.TagType.oneway])
|
if (currentTags.ContainsKey(Tag.TagType.forward) && !(bool)currentTags[Tag.TagType.forward])
|
||||||
{
|
{
|
||||||
if (currentTags.ContainsKey(Tag.TagType.forward) && !(bool)currentTags[Tag.TagType.forward])
|
OsmEdge n21e = new OsmEdge(currentTags[Tag.TagType.id], node2Id, node1Id, nodeRegions[node2Id]);
|
||||||
{
|
WriteWay(ref regionWaysFileStreams, nodeRegions[node2Id], n21e, outputPath);
|
||||||
OsmEdge n21e = new OsmEdge(currentTags[Tag.TagType.id], node2Id, node1Id,
|
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node2Id], currentTags, outputPath);
|
||||||
nodeRegions[node1Id]);
|
|
||||||
WriteWay(ref regionWaysFileStreams, nodeRegions[node2Id], n21e, outputPath);
|
|
||||||
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node2Id],
|
|
||||||
currentTags, outputPath);
|
|
||||||
}
|
|
||||||
else if (currentTags.ContainsKey(Tag.TagType.forward) && (bool)currentTags[Tag.TagType.forward])
|
|
||||||
{
|
|
||||||
OsmEdge n12e = new OsmEdge(currentTags[Tag.TagType.id], node1Id, node2Id,
|
|
||||||
nodeRegions[node2Id]);
|
|
||||||
WriteWay(ref regionWaysFileStreams, nodeRegions[node1Id], n12e, outputPath);
|
|
||||||
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node1Id],
|
|
||||||
currentTags, outputPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
OsmEdge n12e = new OsmEdge(currentTags[Tag.TagType.id], node1Id, node2Id,
|
OsmEdge n12e = new OsmEdge(currentTags[Tag.TagType.id], node1Id, node2Id, nodeRegions[node2Id]);
|
||||||
nodeRegions[node2Id]);
|
|
||||||
WriteWay(ref regionWaysFileStreams, nodeRegions[node1Id], n12e, outputPath);
|
WriteWay(ref regionWaysFileStreams, nodeRegions[node1Id], n12e, outputPath);
|
||||||
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node1Id],
|
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node1Id], currentTags, outputPath);
|
||||||
currentTags, outputPath);
|
|
||||||
|
|
||||||
OsmEdge n21e = new OsmEdge(currentTags[Tag.TagType.id], node2Id, node1Id,
|
|
||||||
nodeRegions[node1Id]);
|
|
||||||
WriteWay(ref regionWaysFileStreams, nodeRegions[node2Id], n21e, outputPath);
|
|
||||||
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node2Id],
|
|
||||||
currentTags, outputPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if(nodeRegions.ContainsKey(node1Id) && nodeRegions.ContainsKey(node2Id))
|
||||||
|
{
|
||||||
|
OsmEdge n12e = new OsmEdge(currentTags[Tag.TagType.id], node1Id, node2Id, nodeRegions[node2Id]);
|
||||||
|
WriteWay(ref regionWaysFileStreams, nodeRegions[node1Id], n12e, outputPath);
|
||||||
|
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node1Id], currentTags, outputPath);
|
||||||
|
|
||||||
|
OsmEdge n21e = new OsmEdge(currentTags[Tag.TagType.id], node2Id, node1Id, nodeRegions[node2Id]);
|
||||||
|
WriteWay(ref regionWaysFileStreams, nodeRegions[node2Id], n21e, outputPath);
|
||||||
|
WriteTags(ref regionTagsFileStreams, ref writtenWayTagsInRegion, nodeRegions[node2Id], currentTags, outputPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user