Beispiel #1
0
        /// <summary>
        /// Parse the given JOSM GeoJson output to extract nodes.
        /// </summary>
        /// <param name="collection">GeoJson from JOSM</param>
        /// <param name="nodes">Extracted nodes</param>
        /// <param name="edges">Extracted edges</param>
        /// <returns>True if successful</returns>
        public static bool ParseNodeSource(FeatureCollection collection, out List <Node> nodes, out List <NodeEdge> edges)
        {
            nodes = new List <Node>(100);
            edges = new List <NodeEdge>(200);

            try
            {
                List <(IPosition, IPosition)> CorridorPointPairs = new List <(IPosition, IPosition)>(200);
                List <Feature> features = collection.Features;

                if (features == null)
                {
                    return(false);
                }

                // Iterate through features.
                foreach (Feature f in features)
                {
                    Node node = new Node();
                    if (f.Properties != null)
                    {
                        // Check for flag property. This is a room/stairs/lift/block node.
                        if (f.Properties.ContainsKey("NAV_FEATURE") && f.Properties.ContainsKey("node_type") && !string.Equals(f.Properties["node_type"].ToString(), "block", StringComparison.OrdinalIgnoreCase))
                        {
                            // We don't need to add block nodes as they are not used for navigation.
                            node.NodeId          = f.Properties["id"].ToString().ToLower().Trim();
                            node.BuildingCode    = f.Properties["building"].ToString().ToLower().Trim();
                            node.Floor           = Convert.ToByte(f.Properties["level"].ToString().ToLower().Trim());
                            node.Latitude        = null;
                            node.Longitude       = null;
                            node.Type            = SharedFunctions.GetNodeType(f.Properties["node_type"].ToString());
                            node.LeafletNodeType = f.Properties["node_type"].ToString().ToLower().Trim();

                            if (node.NodeId[0] == 'm')
                            {
                                node.Type = NodeType.Unrouteable;
                            }
                            // Check for a name.
                            if (f.Properties.ContainsKey("name") && f.Properties["name"] != null)
                            {
                                node.RoomName = f.Properties["name"].ToString();
                            }

                            // Check for connected nodes property.
                            if (f.Properties.ContainsKey("connected_nodes") && f.Properties["connected_nodes"] != null)
                            {
                                string[] cons = f.Properties["connected_nodes"].ToString().ToLower().Split(',');
                                foreach (string con in cons)
                                {
                                    edges.Add(new NodeEdge(node.NodeId, con.ToLower().Trim(), 999999));
                                }
                            }

                            nodes.Add(node);
                        }

                        // Is this feature a line string?
                        if (f.Geometry?.Type == GeoJSON.Net.GeoJSONObjectType.LineString)
                        {
                            LineString linestring = f.Geometry as LineString;
                            if (linestring.Coordinates.Count > 0)
                            {
                                // Iterate through the coordinates.
                                for (int i = 1; i < linestring.Coordinates.Count; i++)
                                {
                                    // Extract all coordinate pairs for later use matching against extracted corridor nodes.
                                    IPosition point1 = linestring.Coordinates[i - 1];
                                    IPosition point2 = linestring.Coordinates[i];
                                    CorridorPointPairs.Add((point1, point2));
                                }
                            }
                        }

                        // Check for flag property.
                        if (f.Properties.ContainsKey("NAV_CORRIDOR"))
                        {
                            // This is a corridor.
                            node.NodeId          = f.Properties["id"].ToString().ToLower().Trim();
                            node.BuildingCode    = f.Properties["building"].ToString().ToLower().Trim();
                            node.Floor           = Convert.ToByte(f.Properties["level"].ToString().ToLower().Trim());
                            node.Latitude        = ((Point)f.Geometry).Coordinates.Latitude;
                            node.Longitude       = ((Point)f.Geometry).Coordinates.Longitude;
                            node.Type            = SharedFunctions.GetNodeType(f.Properties["node_type"].ToString());
                            node.CorridorWidth   = Convert.ToDouble(f.Properties["corridor_width"].ToString());
                            node.LeafletNodeType = f.Properties["node_type"].ToString().ToLower().Trim();

                            if (f.Properties.ContainsKey("name"))
                            {
                                node.RoomName = f.Properties["name"].ToString().Trim();
                            }

                            // Check for connected nodes property.
                            if (f.Properties.ContainsKey("connected_nodes") && f.Properties["connected_nodes"] != null)
                            {
                                string[] cons = f.Properties["connected_nodes"].ToString().ToLower().Split(',');
                                foreach (string con in cons)
                                {
                                    edges.Add(new NodeEdge(node.NodeId, con.ToLower().Trim(), 999999));
                                }
                            }
                            nodes.Add(node);
                        }
                    }
                }// End foreach loop.

                // Sort out line-string connections - Corridors can be connected to each other using
                // a simple line string.
                edges.AddRange(LineStringsToEdges(CorridorPointPairs, nodes, features));
                return(true);
            }
            catch
            {
                return(false);
            }
        }
Beispiel #2
0
        /// <summary>
        /// Verify Node edges on this single floor.
        /// </summary>
        /// <param name="nodes">Nodes on this floor</param>
        /// <param name="edges">Corresponding edges on this floor</param>
        /// <returns>A list of broken connection errors, if any</returns>
        public static List <string> VerifyNodeEdgesSingleFloor(List <Node> nodes, List <NodeEdge> edges)
        {
            List <string> brokenConnections = new List <string>();

            // Iterate through all nodes.
            foreach (Node node in nodes)
            {
                // Ensure this node has connections.
                NodeEdge[] connections = edges.Where(e => e.Node1Id == node.NodeId || e.Node2Id == node.NodeId).ToArray();

                // Iterate through its connections.
                foreach (NodeEdge connection in connections)
                {
                    // Skip connections between stairs and lifts because they require more than one floor to verify.
                    if ((connection.Node1Id.StartsWith("s", StringComparison.OrdinalIgnoreCase) && connection.Node2Id.StartsWith("s", StringComparison.OrdinalIgnoreCase)) ||
                        (connection.Node1Id.StartsWith("l", StringComparison.OrdinalIgnoreCase) && connection.Node2Id.StartsWith("l", StringComparison.OrdinalIgnoreCase)))
                    {
                        continue;
                    }

                    string node1BC    = SharedFunctions.GetBuildingCodeFromId(connection.Node1Id);
                    string node2BC    = SharedFunctions.GetBuildingCodeFromId(connection.Node2Id);
                    sbyte  node1Floor = -1;
                    try
                    {
                        node1Floor = SharedFunctions.GetLevelFromId(connection.Node1Id);
                        if (node1Floor < 0)
                        {
                            throw new InvalidOperationException("No Level!");
                        }
                    }
                    catch
                    {
                        brokenConnections.Add($"{connection.Node1Id} is missing a level number in the connection to {connection.Node2Id}");
                        continue;
                    }
                    sbyte node2Floor = -1;
                    try
                    {
                        node2Floor = SharedFunctions.GetLevelFromId(connection.Node2Id);
                        if (node2Floor < 0)
                        {
                            throw new InvalidOperationException("No Level!");
                        }
                    }
                    catch
                    {
                        brokenConnections.Add($"{connection.Node2Id} is missing a level number in the connection to {connection.Node1Id}");
                        continue;
                    }

                    if (string.IsNullOrWhiteSpace(node1BC))
                    {
                        brokenConnections.Add($"{connection.Node1Id} is missing a building code in its connection to {connection.Node2Id}");
                        continue;
                    }

                    if (string.IsNullOrWhiteSpace(node2BC))
                    {
                        brokenConnections.Add($"{connection.Node2Id} is missing a building code in its connection to {connection.Node1Id}");
                        continue;
                    }

                    // Skip connections between outdoors and indoors - different building, or different floors - codes as they require more than on json file to verify.
                    if (node1BC != node2BC || node1Floor != node2Floor)
                    {
                        continue;
                    }

                    Node node1 = nodes.Find(n => n.NodeId == connection.Node1Id);
                    Node node2 = nodes.Find(n => n.NodeId == connection.Node2Id);

                    // Check both nodes exist.
                    if (node1 == default)
                    {
                        brokenConnections.Add($"Cannot find node: {connection.Node1Id}");
                        continue;
                    }

                    if (node2 == default)
                    {
                        brokenConnections.Add($"Cannot find node: {connection.Node2Id}");
                        continue;
                    }

                    NodeEdge result = Array.Find(connections, c => c.Node1Id == node2.NodeId && c.Node2Id == node1.NodeId);

                    // Check a connection exists both ways.
                    if (result == null)
                    {
                        // Connection does not exist.
                        brokenConnections.Add($"Broken Connection: {node1.NodeId} is only connected to {node2.NodeId} one way");
                    }
                }
            }

            return(brokenConnections);
        }
Beispiel #3
0
        /// <summary>
        /// Check if a feature has a valid id property.
        /// </summary>
        /// <param name="f">Feature to check</param>
        /// <param name="errors">reference to errors list</param>
        /// <param name="warnings">reference to warnings list</param>
        /// <returns>True if valid</returns>
        private static bool IsIdValid(Feature f, List <string> errors, List <string> warnings)
        {
            // Get property values.
            string id       = f.Properties["id"].ToString().ToLower().Trim();
            string level    = f.Properties["level"].ToString().ToLower().Trim();
            string building = f.Properties["building"].ToString().ToLower().Trim();
            string nodeType = f.Properties["node_type"].ToString().ToLower().Trim();

            // Split Id string.
            string[]      idParts      = id.Split('_');
            StringBuilder buildingCode = new StringBuilder();
            StringBuilder floor        = new StringBuilder();

            string idNodeType;

            if (idParts.Length > 0)
            {
                // Get the node type character.
                idNodeType = idParts[0];
                if (idParts.Length > 1)
                {
                    // Get the building code and floor number string.
                    string buildingFloorCode = idParts[1];
                    // Get building code and floor number separately.
                    for (int i = 0; i < buildingFloorCode.Length; i++)
                    {
                        if (char.IsLetter(buildingFloorCode[i]))
                        {
                            buildingCode.Append(buildingFloorCode[i]);
                        }
                        else if (char.IsDigit(buildingFloorCode[i]))
                        {
                            floor.Append(buildingFloorCode[i]);
                        }
                    }
                }
                else
                {
                    errors.Add($"Could not find a valid id for feature.");
                    return(false);
                }
            }
            else
            {
                errors.Add($"Could not find a valid id for feature.");
                return(false);
            }

            // Check node ID matches name if it is a routable room.
            if (nodeType == "room" || nodeType == "other")
            {
                string roomCodeFromId = SharedFunctions.GetRoomCodeFromId(id);
                string name           = f.Properties["name"].ToString();
                if (!name.StartsWith(roomCodeFromId, StringComparison.OrdinalIgnoreCase))
                {
                    warnings.Add($"The room {id} has a potentially invalid name: '{name}' for the given id. Human Check Required. (Consider if this node should be named with the room code or not)");
                }
            }

            // Validation checks.
            if (idNodeType == "r" && nodeType != "room" && nodeType != "other")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "m" && nodeType != "room" && nodeType != "other")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "s" && nodeType != "stairs")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "l" && nodeType != "lift")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "c" && nodeType != "corridor")
            {
                errors.Add($"The corridor node {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "p" && nodeType != "parking")
            {
                errors.Add($"The parking node {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "wcm" && nodeType != "wcm")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "wcf" && nodeType != "wcf")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "wcb" && nodeType != "wcb")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "wcd" && nodeType != "wcd")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (idNodeType == "wcmf" && nodeType != "wcmf")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }

            if (idNodeType == "wcn" && nodeType != "wcn")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }

            if (idNodeType == "wcs" && nodeType != "wcs")
            {
                errors.Add($"The room {id} does not have a valid node_type tag");
                return(false);
            }
            if (building != buildingCode.ToString())
            {
                errors.Add($"The feature {id} does not have a valid building tag");
                return(false);
            }
            if (floor.ToString() != level)
            {
                errors.Add($"The feature {id} does not have a valid level tag");
                return(false);
            }
            return(true);
        }
Beispiel #4
0
        /// <summary>
        /// Calculate the weights of each edge.
        /// </summary>
        /// <param name="nodes">A list of all nodes involved</param>
        /// <param name="edges">A list of all edges to weigh</param>
        /// <returns>A list of edges with correct weights</returns>
        public static List <NodeEdge> CalculateWeights(List <Node> nodes, List <NodeEdge> edges, EdgeCaseWeightsConfiguration edgeCases)
        {
            // Iterate through edges
            foreach (NodeEdge edge in edges)
            {
                // Default weight is high to avoid routing weird ways - any arbitrary high number will do.
                double weight = 999999;
                double?area   = null;

                // Lower weight for stairs to prioritise them when changing floors.
                if (edge.Node1Id.StartsWith("s_") || edge.Node2Id.StartsWith("s_"))
                {
                    weight = 888888;
                }

                // Get nodes for each side of the edge.
                Node node1 = nodes.Find(n => n.NodeId == edge.Node1Id);
                Node node2 = nodes.Find(n => n.NodeId == edge.Node2Id);

                double distance;
                if (node1 != null && node2 != null)
                {
                    if (node1.Latitude.HasValue && node2.Latitude.HasValue && node1.Longitude.HasValue && node2.Longitude.HasValue)
                    {
                        // Calculate weight from physical distance.
                        distance = SharedFunctions.GetDistanceFromLatLonInMeters(node1.Latitude.GetValueOrDefault(), node1.Longitude.GetValueOrDefault(), node2.Latitude.GetValueOrDefault(), node2.Longitude.GetValueOrDefault());
                        weight   = distance;
                    }
                    else
                    {
                        distance = SharedFunctions.NonCorridorDistance;
                    }

                    if (node1.CorridorWidth != null && node2.CorridorWidth != null)
                    {
                        // Calculate average area between points.
                        // Get average width.
                        double width = (node1.CorridorWidth.GetValueOrDefault() + node2.CorridorWidth.GetValueOrDefault()) / 2;
                        // Calculate area.
                        area = width * distance;
                    }
                }

                // Handle special cases.
                // Iterate through either node list.
                foreach (EitherNodeEntry item in edgeCases.EitherNode)
                {
                    if (NodeIdStartsWith(edge.Node1Id, item.StringStart) || NodeIdStartsWith(edge.Node2Id, item.StringStart))
                    {
                        weight = item.Weight;
                    }
                }

                // Iterate through both nodes list.
                foreach (BothNodesEntry item in edgeCases.BothNodes)
                {
                    if (NodeIdStartsWith(edge.Node1Id, item.Node1String) && NodeIdStartsWith(edge.Node2Id, item.Node2String))
                    {
                        weight = item.Weight;
                    }
                }

                edge.Weight       = weight;
                edge.CorridorArea = area;
            }
            return(edges);
        }
Beispiel #5
0
        /// <summary>
        /// Use the A* algorithm to calculate a route, adjusted for congestion.
        /// </summary>
        /// <param name="start">Start node id</param>
        /// <param name="end">End node id</param>
        /// <param name="allStudentRoutes">A collection of all student routes for today.</param>
        /// <param name="startingTime">The requested starting time of this route.</param>
        /// <returns>A route object with the calculated route</returns>
        /// <exception cref="Exception"></exception>
        public Route BuildAStar(string start, string end, StudentRoute[] allStudentRoutes, TimeSpan startingTime)
        {
            // Check if the start is the same as the end, or either the start or end are invalid nodes.
            if (start == end || !AllNodes.ContainsKey(start) || !AllNodes.ContainsKey(end))
            {
                // return an empty route
                return(new Route());
            }

            // Get the start node object from its id.
            Node startNode = AllNodes[start];
            // Get the end node object from its id.
            Node endNode = AllNodes[end];

            // Find the closest corridor node from the end node - this is used by A* as the lat/long end point for heuristic function.
            // This is often one of the direct connections most cases, however we may need to look further into the graph for outlier cases.
            Node endCorridor = FindNextCorridorNode(endNode, AllNodes);

            // Initialise the frontier priority queue.
            FastPriorityQueue <AStarNode> frontier = new FastPriorityQueue <AStarNode>(AllNodes.Count);

            // Initialise a lookup table for the queue.
            Dictionary <string, AStarNode> queueTable = new Dictionary <string, AStarNode>();

            // Heuristic for A* algorithm - Calculate the distance between the given node and the end corridor, or default to 1 if the given node is not a corridor.
            float heuristicFunction(Node node) => node.Type == NodeType.Corridor || node.Type == NodeType.Parking ? (float)node.DistanceInMetersTo(endCorridor) : SharedFunctions.NonCorridorDistance;

            // Instantiate an AStarNode from the start node.
            AStarNode starter = new AStarNode(startNode, null, 0f);

            // Add to the queue table
            queueTable.Add(startNode.NodeId, starter);
            // Add to the priority queue.
            frontier.Enqueue(starter, heuristicFunction(startNode));

            // Loop until the priority queue is empty.
            while (frontier.Count > 0)
            {
                // Dequeue the lowest cost node.
                AStarNode current = frontier.Dequeue();
                // Get its true cost.
                float currentCost = current.TrueCost;

                // Check if the current node is the end corridor.
                if (current.Node.NodeId == endCorridor.NodeId)
                {
                    // Route found.
                    // Backtrack to build the route.
                    List <Node> route = BackTrack(current);
                    // Add end node as we only routed to the nearest corridor.
                    route.Add(endNode);
                    // Return the final route.
                    return(new Route(route, current.TrueCost));
                }

                // Mark the current node as visited.
                current.MarkVisited();

                AStarNode currentNode = current;

                double distanceToThisPoint = 0;

                // Calculate the time spent getting to this point.
                while (currentNode.LeadingNode != null)
                {
                    // Check this is a corridor node.
                    if (currentNode.Node.Type == NodeType.Corridor && currentNode.LeadingNode.Node.Type == NodeType.Corridor)
                    {
                        // Get the distance to the leading node.
                        distanceToThisPoint += currentNode.Node.DistanceInMetersTo(currentNode.LeadingNode.Node);
                    }
                    else
                    {
                        distanceToThisPoint += SharedFunctions.NonCorridorDistance;
                    }

                    currentNode = currentNode.LeadingNode;
                }

                // Calculate the time taken to get to this point.
                double timeElapsed = SharedFunctions.CalculateWalkingTimeNoRounding(distanceToThisPoint);

                // Calculate the time of day from the elapsed time.
                TimeSpan currentTime = startingTime.Add(TimeSpan.FromSeconds(timeElapsed));

                // Calculate the occupancies at this point.
                Dictionary <(string entryId, string exitId), int> edgeOccupancies = CongestionHelper.CalculateEdgeOccupanciesAtTime(allStudentRoutes, currentTime);

                // Iterate through all edges of the current node.
                foreach (NodeEdge edge in current.Node.OutgoingEdges)
                {
                    // End node of edge.
                    Node edgeEnd = edge.Node2;

                    // Check if the node already exists in the queue table.
                    bool edgeEndAlreadyPresent = queueTable.TryGetValue(edgeEnd.NodeId, out AStarNode target);
                    // Check if we have already visited this node.
                    bool alreadyVisitedEdgeEnd = edgeEndAlreadyPresent && target.Visited;

                    // If we have already visited this node, skip it.
                    if (alreadyVisitedEdgeEnd)
                    {
                        continue;
                    }

                    float congestionMultiplier = CalculateCongestionMultiplier(edge, edgeOccupancies);

                    // Calculate the true cost of this edge.
                    float trueCost = currentCost + ((float)edge.Weight * congestionMultiplier);
                    // Calculate the heuristic cost of this node.
                    float heuristicCost = heuristicFunction(edgeEnd);

                    // Total the costs and use the congestion multiplier.
                    float combinedCost = trueCost + heuristicCost;

                    // Is the edge already in the queue?
                    if (edgeEndAlreadyPresent)
                    {
                        // Only use new route if it is better.
                        if (combinedCost < target.TrueCost)
                        {
                            // It is better, update the nodes in the queue.
                            target.Update(current, trueCost);
                            frontier.UpdatePriority(target, combinedCost);
                        }
                    }
                    else
                    {
                        // It is not in the queue, instantiate an AStarNode object.
                        target = new AStarNode(AllNodes[edgeEnd.NodeId], current, trueCost);
                        // Add to the queue table.
                        queueTable.Add(edgeEnd.NodeId, target);
                        // Add to the queue.
                        frontier.Enqueue(target, combinedCost);
                    }
                }
            }
            // No route could be found.
            return(new Route());
        }