Click here to Skip to main content
15,882,017 members
Articles / Programming Languages / C# 4.0

A Complex - Yet Quite Intelligible - Pathfinding in C#

Rate me:
Please Sign up or sign in to vote.
4.96/5 (41 votes)
30 Aug 2013CPOL21 min read 59.4K   2.5K   88  
A semi-analytic 2D path-finding for large dynamic scenarios.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using YinYang.CodeProject.Projects.SimplePathfinding.Helpers;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.AStar;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.BreadthFirst;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.DepthFirst;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.Dijkstra;
using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.Evasion;
using YinYang.CodeProject.Projects.SimplePathfinding.Properties;
using YinYang.CodeProject.Projects.SimplePathfinding.Scenarios;

namespace YinYang.CodeProject.Projects.SimplePathfinding
{
    public partial class MainForm : Form
    {
        #region | Constant |

        private const Int32 DefaultAreaWidth = 512;
        private const Int32 DefaultAreaHeight = 512;

        #endregion

        #region | Fields |

        private readonly Func<CheckBox, Boolean> getOption;

        private readonly Action processTasks;
        private readonly Action<Image> updateImage;
        private readonly Action<String> updateCaption;

        private readonly Func<Point, Point> getFormPosition;

        private Int32 objectDiameter;
        private Thread thread;
        private Graphics graphics;
        private Bitmap defaultImage;
        private Boolean turnOnEvents;
        private Graphics imageGraphics;

        private PathScenario activeScenario;
        private BasePathfinder activePathFinder;

        private List<PathScenario> scenarioList;
        private List<BasePathfinder> pathFinderList;

        private readonly UInt16[,] distanceMap;
        private readonly List<Action> taskList;
        private readonly StopFunction stopFunction;

        #endregion

        #region | Constructors |

        /// <summary>
        /// Initializes a new instance of the <see cref="MainForm"/> class.
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            taskList = new List<Action>();

            processTasks = ProcessTasks;
            objectDiameter = trackObjectDiameter.Value;
            getOption = checkbox => checkbox.Checked;
            updateCaption = caption => Text = caption;
            updateImage = image => graphics.DrawImageUnscaled(image, 0, 0);

            getFormPosition = PointToClient;

            MouseUp += (sender, args) => { if (args.Button == MouseButtons.Right) taskList.Add(() => RecreateDefaultImage()); };
            ClientSize = new Size(DefaultAreaWidth + 250, DefaultAreaHeight);
            distanceMap = new UInt16[DefaultAreaWidth, DefaultAreaHeight];

            // creates functions
            stopFunction = (point, ignoreRadius) =>
            {
                Color color = Color.Black;
                Int32 x = point.X;
                Int32 y = point.Y;
                UInt16 radius = 1;

                if (x >= 0 && y >= 0 && x < defaultImage.Width && y < defaultImage.Height)
                {
                    color = defaultImage.GetPixel(x, y);
                    radius = distanceMap[x, y];
                }

                return color.ToArgb() == Color.Black.ToArgb() || (!ignoreRadius && radius <= (objectDiameter + 1) >> 1);
            };
        }

        #endregion

        #region | Process path-finding scenario |

        private void ProcessScenario()
        {
            // position starts at [0, 0]
            Point position = Point.Empty;
            Invoke(processTasks);

            using (Bitmap image = new Bitmap(DefaultAreaWidth, DefaultAreaHeight, graphics))
            {
                imageGraphics = Graphics.FromImage(image);
                imageGraphics.SmoothingMode = SmoothingMode.HighQuality;

                do // repeats forever
                {
                    // starts measuring total time
                    DateTime startTime = DateTime.Now;

                    // prepares parameters
                    Point cursor = (Point) Invoke(getFormPosition, MousePosition);

                    // changes start position
                    if (MouseButtons.HasFlag(MouseButtons.Middle)) position = cursor;

                    // performs path finding with a given path finder
                    TimeSpan pathFindTime;
                    Int32 pointCount = DrawPath(image, imageGraphics, position, cursor, out pathFindTime);

                    // draws the markers
                    PathScenario.DrawMarker(imageGraphics, position, Brushes.WhiteSmoke, true);
                    PathScenario.DrawMarker(imageGraphics, cursor, Brushes.WhiteSmoke, true);

                    // updates the form information
                    Invoke(updateCaption, string.Format("Cursor: [{0:000}, {1:000}], Points: {2:0000}, Path finding time: {3:ss\\:ffffff}, Total time: {4:ss\\:ffffff}", cursor.X, cursor.Y, pointCount, pathFindTime, DateTime.Now - startTime));
                    Invoke(updateImage, image);

                    // restores the default image look
                    imageGraphics.DrawImageUnscaled(defaultImage, Point.Empty);

                    Invoke(processTasks);
                }
                while (true);
            }
        }

        #endregion

        #region | Methods |

        private void ChangePathFinder()
        {
            activePathFinder = pathFinderList[listMethod.SelectedIndex];
        }

        private void ChangeScenario()
        {
            activeScenario = scenarioList[listScenarios.SelectedIndex];

            taskList.Add(() => RecreateDefaultImage(false));
        }

        private void RecreateDefaultImage(Boolean generateNew = true)
        {
            Text = Resources.MainForm_RecreateScenario_WaitCaption;

            // disposes of old default image
            DisposeDefaultImage();

            // creates new default image for this scenario
            Boolean flagMinimizeHollowAreas = (Boolean) Invoke(getOption, checkMinimizeHollowAreas);
            defaultImage = activeScenario.CreateDefaultImage(generateNew, flagMinimizeHollowAreas);

            UpdateDistanceMap();
        }

        private void UpdateDistanceMap()
        {
            // cannot create map for invalid image, just skip it
            if (defaultImage == null) return;

            // prepares for the processing
            List<Point> nextRound = new List<Point>();

            // resets the distance map to Empty = 0, Blocked = 1
            for (Int32 y = 0; y < DefaultAreaHeight; y++)
            for (Int32 x = 0; x < DefaultAreaWidth; x++)
            {
                Point point = new Point(x, y);

                // determines whether the pixel is blocked, or not
                if (stopFunction(point, true))
                {
                    distanceMap[x, y] = 1;
                    nextRound.Add(point);
                }
                else
                {
                    distanceMap[x, y] = 0;
                }
            }

            // round to does actual classification
            UInt16 round = 2;
        
            do // perform rounds if there is still at least one pixel to process
            {
                // switches the next round for current round
                List<Point> thisRound = nextRound;
                nextRound = new List<Point>();

                // processes all the neighbor points along the border of already classified pixels
                foreach (Point point in thisRound)
                foreach (DirectionType direction in DirectionHelper.GetValues())
                {
                    Point neighborPoint = DirectionHelper.GetNextStep(point, direction);

                    // checks whether it is within valid bounds
                    if (neighborPoint.X >= 0 && neighborPoint.Y >= 0 &&
                        neighborPoint.X < DefaultAreaWidth && neighborPoint.Y < DefaultAreaHeight)
                    {
                        UInt16 value = distanceMap[neighborPoint.X, neighborPoint.Y];

                        // if this neighbor is still unclassified, do it, and add it for the next round
                        if (value == 0)
                        {
                            if ((Boolean) Invoke(getOption, checkShowDistanceMap))
                            {
                                Int32 intensity = 255 - round%254;
                                Color color = Color.FromArgb(255, 255, 255, intensity);
                                defaultImage.SetPixel(neighborPoint.X, neighborPoint.Y, color);
                            }

                            distanceMap[neighborPoint.X, neighborPoint.Y] = round;
                            nextRound.Add(neighborPoint);
                        }
                    }
                }

                round++;
            }
            while (nextRound.Count > 0);
        }

        private void ProcessTasks()
        {
            if (taskList.Count > 0)
            {
                try
                {
                    foreach (var action in taskList)
                    {
                        action();
                    }
                }
                finally
                {
                    taskList.Clear();
                }
            }
        }

        private void DisposeDefaultImage()
        {
            if (defaultImage != null)
            {
                defaultImage.Dispose();
                defaultImage = null;
            }
        }

        #endregion

        #region | Common |

        protected Int32 DrawPath(Bitmap image, Graphics graphics, Point start, Point end, out TimeSpan time)
        {
            // performs boundary check first
            PathScenario.CheckBounds(ref start, ref end, DefaultAreaWidth, DefaultAreaHeight);

            // determines the options
            Boolean flagPerformOptimizations = (Boolean) Invoke(getOption, checkPerformOptimization);
            Boolean flagDrawPivotPoints = (Boolean) Invoke(getOption, checkDrawPivotPoints);
            Boolean flagDrawLargerPivots = (Boolean) Invoke(getOption, checkDrawLargerPivots);

            // initializes the parameters
            Int32 result = 0;
            IReadOnlyCollection<Point> points;
            IReadOnlyCollection<Point> pivotPoints;

            // finds the path while measuring time elapsed
            DateTime startDate = DateTime.Now;
            Single precisionAlignment = objectDiameter%2 == 0 ? 0.5f : 0.0f;
            Pen pen = new Pen(Color.DarkGreen) { Width = objectDiameter - precisionAlignment, StartCap = LineCap.Round, EndCap = LineCap.Round, LineJoin = LineJoin.Round };

            Boolean found = activePathFinder.TryFindPath(start, end, stopFunction, out points, out pivotPoints, flagPerformOptimizations);
            time = DateTime.Now - startDate;

            if (found)
            {
                // draws connected lines through all the points
                if (points.Count > 1)
                {
                    graphics.DrawLines(pen, points.ToArray());
                }

                // draws markers at pivot points (if turned on)
                if (flagDrawPivotPoints && pivotPoints != null)
                {
                    foreach (Point highlightPoint in pivotPoints)
                    {
                        if (flagDrawLargerPivots)
                        {
                            PathScenario.DrawMarker(graphics, highlightPoint, Brushes.Red, true);
                        }
                        else
                        {
                            image.SetPixel(highlightPoint.X, highlightPoint.Y, Color.Red);
                        }
                    }
                }

                result = points.Count;
            }

            return result;
        }

        #endregion

        #region << Form >>

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            graphics = CreateGraphics();

            ThreadStart operation = ProcessScenario;
            thread = new Thread(operation);
            thread.Start();
        }

        protected override void OnKeyUp(KeyEventArgs e)
        {
            base.OnKeyUp(e);

            if (e.KeyCode == Keys.D1) listMethod.SelectedIndex = 0;
            if (e.KeyCode == Keys.D2) listMethod.SelectedIndex = 1;
            if (e.KeyCode == Keys.D3) listMethod.SelectedIndex = 2;
            if (e.KeyCode == Keys.D4) listMethod.SelectedIndex = 3;
            if (e.KeyCode == Keys.D5) listMethod.SelectedIndex = 4;

            if (e.KeyCode == Keys.O) checkPerformOptimization.Checked = !checkPerformOptimization.Checked;
            if (e.KeyCode == Keys.P) checkDrawPivotPoints.Checked = !checkDrawPivotPoints.Checked;
            if (e.KeyCode == Keys.H) checkMinimizeHollowAreas.Checked = !checkMinimizeHollowAreas.Checked;
            if (e.KeyCode == Keys.L) checkDrawLargerPivots.Checked = !checkDrawLargerPivots.Checked;
            if (e.KeyCode == Keys.Q) checkUseHighQuality.Checked = !checkUseHighQuality.Checked;
            if (e.KeyCode == Keys.W) checkShowDistanceMap.Checked = !checkShowDistanceMap.Checked;

            if (e.KeyCode == Keys.Add) trackObjectDiameter.Value = Math.Max(trackObjectDiameter.Minimum, Math.Min(trackObjectDiameter.Maximum, trackObjectDiameter.Value + 5));
            if (e.KeyCode == Keys.Subtract) trackObjectDiameter.Value = Math.Max(trackObjectDiameter.Minimum, Math.Min(trackObjectDiameter.Maximum, trackObjectDiameter.Value - 5));
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            if (thread != null)
            {
                thread.Abort();
            }

            DisposeDefaultImage(); 
           
            base.OnClosing(e);
        }

        #endregion

        #region << Events >>

        private void MainFormLoad(object sender, EventArgs e)
        {
            pathFinderList = new List<BasePathfinder>
            {
                new AStarPathfinder(DefaultAreaWidth, DefaultAreaHeight),
                new BreadthFirstPathfinder(DefaultAreaWidth, DefaultAreaHeight),
                new DepthFirstPathfinder(DefaultAreaWidth, DefaultAreaHeight),
                new DijkstraPathfinder(DefaultAreaWidth, DefaultAreaHeight),
                new EvasionPathfinder(),
            };

            scenarioList = new List<PathScenario>
            {
                new RandomRectangleScenario(DefaultAreaWidth, DefaultAreaHeight),
                new RandomMarkerScenario(DefaultAreaWidth, DefaultAreaHeight),
                new RandomEllipseScenario(DefaultAreaWidth, DefaultAreaHeight),
                new RandomLineScenario(DefaultAreaWidth, DefaultAreaHeight),
                new BlackObeliskScenario(DefaultAreaWidth, DefaultAreaHeight),
                new ImageFileScenario(DefaultAreaWidth, DefaultAreaHeight),
            };

            turnOnEvents = false;

            listMethod.SelectedIndex = 4; // 0
            listScenarios.SelectedIndex = 0; // 0

            turnOnEvents = true;

            ChangePathFinder();
            ChangeScenario();
        }

        private void ListMethodSelectedIndexChanged(object sender, EventArgs e)
        {
            if (turnOnEvents) ChangePathFinder();
        }

        private void ListScenariosSelectedIndexChanged(object sender, EventArgs e)
        {
            if (turnOnEvents) ChangeScenario();
        }

        private void TrackDiameterValueChanged(object sender, EventArgs e)
        {
            taskList.Add(() => { objectDiameter = trackObjectDiameter.Value; });
        }

        private void CheckMinimizeHollowAreasCheckedChanged(object sender, EventArgs e)
        {
            taskList.Add(() => RecreateDefaultImage(false));
        }

        private void CheckUseHighQualityCheckedChanged(object sender, EventArgs e)
        {
            taskList.Add(() =>
            {
                imageGraphics.SmoothingMode = checkUseHighQuality.Checked ? SmoothingMode.HighQuality : SmoothingMode.HighSpeed;
                imageGraphics.InterpolationMode = checkUseHighQuality.Checked ? InterpolationMode.HighQualityBicubic : InterpolationMode.Low;
            });
        }

        private void CheckShowDistanceMapCheckedChanged(object sender, EventArgs e)
        {
            taskList.Add(() => RecreateDefaultImage(false));
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Czech Republic Czech Republic
Contacts: EMAIL - smartk8@gmail.com

Comments and Discussions