//////////////////////////////////////////////////////////////// // // Copyright (c) 2007-2010 MetaGeek, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////// using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Xml; using System.Collections; using System.IO; using System.Threading; using Inssider.Properties; namespace Inssider { public partial class KmlExporterForm : Form { #region Private Data /// /// Represents a Waypoint node in a GPX document. /// private class gpxWaypoint { /* * More obscure details such as Magnetic Variation and Geoid height are not implemented. * However the NmeaParser does implement them and the data could be in the log file. * */ //Global Coordinates in metric degrees public double latitude; public double longitude; //Elevation/Altitude in metres public double elevation; //UTC as read from the GPS satellites public string time; //Cheekily used at present to record the speed in km/h public string cmt; //type of fix : none, 2d or 3d. public string fix; //the current number of satellites used in the position calculation public int numSatellites; //Horizontal Dilution of Precision public double horizontalDilution; //Vertical Dilution of Precision public double verticalDilution; //Position Dilution of Precision public double positionDilution; //The node of the GPX format is used to store the Access Point information public accessPointMeasurement extension; private string _name = string.Empty; public string name { get { if (null != extension.SSID) { return XmlCleanUp.CleanUp(extension.SSID); } else { return _name; } } set { _name = XmlCleanUp.CleanUp(value); } } private string _description = string.Empty; //Description as recorded by the GPX logger. This is discarded and a new more detailed description is generated. public string description { get { if (_description == string.Empty) { //Generates the description string string desc = name + " [" + extension.MAC + "]" + System.Environment.NewLine + extension.privacy + System.Environment.NewLine + extension.RSSI + "dBm" + System.Environment.NewLine + "Channel " + extension.ChannelID + System.Environment.NewLine + "GPS" + System.Environment.NewLine + " Lat,Lon,Alt " + latitude + "," + longitude + "," + elevation + System.Environment.NewLine + " Speed (km/h) " + cmt + System.Environment.NewLine + " Time (UTC) " + time + System.Environment.NewLine + " Precision " + System.Environment.NewLine + " Satellite Count " + numSatellites + System.Environment.NewLine + " Fix Mode " + fix + System.Environment.NewLine + " VDOP " + verticalDilution + System.Environment.NewLine + " HDOP " + horizontalDilution + System.Environment.NewLine + " PDOP " + positionDilution + System.Environment.NewLine; _description = desc; } return _description; } } } /// /// Represents an Access Point measurement. Contains information such as SSID, Signal Level, Channel, Encryption etc. /// private struct accessPointMeasurement { //Machine Address Code of the Access Points public string MAC; //Name of the Access Point public string SSID; //Signal Strength of the measurement in dBm public int RSSI; //Frequency band the AP is using. public uint ChannelID; //Encryption settings. public string privacy; //Quality of the signal. Similar to RSSI. public uint signalQuality; //Network type : AD-HOC, Infrastructure etc. public string networkType; //Supported data rates. More accurately this should be an array. public string rates; } //Contains all waypoints imported from all GPX files. gpxWaypoint[] gpxWaypoints; //The length of gpxWaypoints int dataPoints; //The waypoints for each individual file. ArrayList[] gpxWaypointsByFile; //Contains the MAC address of gpxWaypointsByFile. This is used as a key to sort it so that all an access points measurements are adjacent in the array. //Beneficial for finding unique APs quickly. ArrayList[] gpxMACAddresses; //2D-Array with a list of Access Points for each channel and encryption type. //Beneficial for outputting the KML files as raw text rather than having to treat them as XML (faster). ArrayList[,] channelAndEncryptionIdxs = new ArrayList[MAX_CHANNELS, ENCRYPTION.Length]; //The filenames of all files selected from the FileDialog. string[] fileNames = new string[1]; //Indicates whether that file contains valid GPX data. bool[] isFileValid; //Encryption/Privacy settings this program recognises as unique types of encryption. static string[] ENCRYPTION = { "Unknown", "None", "WEP", "WPA", "WPA-TKIP", "WPA2" }; //Reset events for the threads which load and process the input and output files. ManualResetEvent[] resetEvents = {new ManualResetEvent(false)}; //The MAC and SSID for each unique Access Point. //These are uses as keys for sorting. string[] mac; string[] SSID; //Array of all unique Access Points. //It is a gpxWaypoint which contains the values of the strongest signal point. gpxWaypoint[] apList; //As apList is sorted by MAC address. Access Points occupy continious blocks in the gpxWaypoints array. //These two variables specify at what index in that array this Access Points starts and stops. int[] apStart; int[] apStop; //Number of individual AP KML files which remain to be output. //When this equals zero the ResetEvent will be triggered & all files will have been output. int numBusy = 0; //STATISTICS //The maximum number of channels allowed. 14 thanks to Japan. static int MAX_CHANNELS =165; // The number of access points which use a given channel. int[] channelCounts = new int[MAX_CHANNELS]; //The number of access points which use a given type of encryption int[] encryptionCounts = new int[ENCRYPTION.Length]; //EXPORTING //The folder to which all files will be exported. string exportFolder = ""; // Does the user what to color WPA-TKIP APs red instead of orange // APs with mixed WPA-WPA2 encryptions will often show up as WPA-TKIP when using ndis driver mode. // If the AP is mixed WPA-WPA2, then WPA is allowed, so color as lowest security option available, // which is WPA const bool isTkipRed = false; //According to the user-specified criteria, these items in the gpxWaypoints array should be discarded. bool[] ignoreWaypoint; //According to the user-specified criteria, this AP has no valid waypoints measurements at all. //That is, all the ignoreWaypoints corresponding to this AP are true. bool[] ignoreAP; //The folder to which the individual AP KML files will be output. string apFolder; //Should the markers be shown with altitude or not? const bool hasAltitude = false; //Does the user what dBm labels on all the markers? bool showLabels = false; //Global variables of the combo-boxes to allow cross-thread reading of their values. int filesSummaryComboBox_SelectedIndex = 0; #endregion #region Constructors public KmlExporterForm() { InitializeComponent(); } #endregion #region File Functions /// /// Loads GPX file of name filenames[e] to gpxWaypointsByFile[e] /// /// index to be processed. private void loadGpxFile(string fileName, int index) { //The user may or may not have selected a valid XML file. //If the file is invalid, indicate this in the isValidFile array. try { //XmlTextReader is many times faster than XmlDocument.Load() XmlTextReader GPX_Reader = new XmlTextReader(fileName); //List of GPX waypoints that have been read in. ArrayList gpxPoints = new ArrayList(); //With their corresponding MAC addresses for use as keys. ArrayList gpxMACs = new ArrayList(); do { string name = GPX_Reader.Name; string value = GPX_Reader.Value; string subnodeName = ""; if (name == "wpt") { //The new waypoint which will eventually be added to gpxWaypoints array. gpxWaypoint newWPT = new gpxWaypoint(); //latitude and longitude as attributes rather than elements. if (GPX_Reader.HasAttributes) { int attrCount = GPX_Reader.AttributeCount; newWPT.latitude = System.Convert.ToDouble(GPX_Reader.GetAttribute("lat")); newWPT.longitude = System.Convert.ToDouble(GPX_Reader.GetAttribute("lon")); } GPX_Reader.Read(); do { //name of the element currently being read in subnodeName = GPX_Reader.Name; switch (subnodeName) { case "ele": newWPT.elevation = System.Convert.ToDouble(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "time": newWPT.time = GPX_Reader.ReadString(); GPX_Reader.Read(); break; case "name": newWPT.name = GPX_Reader.ReadString(); GPX_Reader.Read(); break; case "cmt": newWPT.cmt = GPX_Reader.ReadString(); GPX_Reader.Read(); break; //Description is not read from GPX. It is remade by the LogViewer. case "desc": GPX_Reader.Read(); GPX_Reader.Read(); break; case "fix": newWPT.fix = GPX_Reader.ReadString(); GPX_Reader.Read(); break; case "sat": newWPT.numSatellites = System.Convert.ToInt32(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "vdop": newWPT.verticalDilution = System.Convert.ToDouble(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "hdop": newWPT.horizontalDilution = System.Convert.ToDouble(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "pdop": newWPT.positionDilution = System.Convert.ToDouble(GPX_Reader.ReadString()); GPX_Reader.Read(); break; //Blank tags should be ignored case "": break; //Closing tag of this element. case "wpt": break; case "extensions": GPX_Reader.Read(); string subsubnodeName = ""; //The extensions element specifies information about our access point at this spot. accessPointMeasurement newAPmeasurement = new accessPointMeasurement(); do { subsubnodeName = GPX_Reader.Name; switch (subsubnodeName) { case "MAC": string macAddress = GPX_Reader.ReadString(); gpxMACs.Add(macAddress); newAPmeasurement.MAC = macAddress; GPX_Reader.Read(); break; case "SSID": newAPmeasurement.SSID = (GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "RSSI": newAPmeasurement.RSSI = System.Convert.ToInt32(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "ChannelID": newAPmeasurement.ChannelID = System.Convert.ToUInt32(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "privacy": newAPmeasurement.privacy = GPX_Reader.ReadString(); GPX_Reader.Read(); break; case "signalQuality": newAPmeasurement.signalQuality = System.Convert.ToUInt32(GPX_Reader.ReadString()); GPX_Reader.Read(); break; case "networkType": newAPmeasurement.networkType = GPX_Reader.ReadString(); GPX_Reader.Read(); break; case "rates": newAPmeasurement.rates = GPX_Reader.ReadString(); GPX_Reader.Read(); break; //Blank tags should be ignored. case "": break; //Closing tag should be skipped over also. case "extensions": break; default: GPX_Reader.Read(); GPX_Reader.Read(); break; } GPX_Reader.Read(); } while (subsubnodeName != "extensions"); newWPT.extension = newAPmeasurement; break; //Any other elements of GPX not implemented here but possibilty present in the log. //e.g. magvar,geoidheight,src,link,sym,type,ageofdgpsdata,dgpsid default: GPX_Reader.Read(); GPX_Reader.Read(); break; } GPX_Reader.Read(); } while (subnodeName != "wpt"); gpxPoints.Add(newWPT); } } while (GPX_Reader.Read()); //Save the parsed values gpxWaypointsByFile[index] = gpxPoints; gpxMACAddresses[index] = gpxMACs; //If you made it this far, the file is valid. isFileValid[index] = true; } //Something happened, probably a non-XML file was selected. catch (XmlException) { isFileValid[index] = false; } } /// /// /// private void loadInputFiles() { //Continue no further unless there is at least one file to process. if (fileNames.Length > 0) { //INITILISATION OF VARIABLES //Initialised the isFileValid array. Files are assumed to be invalid until they are loaded correctly. isFileValid = new bool[fileNames.Length]; //These arrays contain the gpxWaypoints and mac address so each file. gpxWaypointsByFile = new ArrayList[fileNames.Length]; gpxMACAddresses = new ArrayList[fileNames.Length]; //For every file, queue a thread in the pool. for (int i = 0; i < fileNames.Length; i++) { loadGpxFile(fileNames[i], i); } //A List of all waypoints and mac addresses. ArrayList gpxList = new ArrayList(); ArrayList gpxMACList = new ArrayList(); //How many of the files chosen by the user were actually valid. int validFileCount = 0; //Keep appending the waypoints from each file to the waypoint and MAC lists if the file is valid. for (int i = 0; i < fileNames.Length; i++) { if (isFileValid[i]) { gpxList.AddRange(gpxWaypointsByFile[i]); gpxMACList.AddRange(gpxMACAddresses[i]); validFileCount++; } } //gpxWaypoints is derived from the gpxList gpxWaypoints = (gpxWaypoint[])gpxList.ToArray(typeof(gpxWaypoint)); //The total number of datapoints from all the files combined. dataPoints = gpxWaypoints.Length; //If at least one of the files was a valid GPX which contained at least one valid waypoint if (dataPoints > 0 && validFileCount > 0) { //key used to sort gpxWaypoint array. mac = (string[])gpxMACList.ToArray(typeof(string)); //gpxWaypoints will now contain the measurements for each access point in contigious blocks. Array.Sort(mac, gpxWaypoints); //temporary list of unique access points and their start and stop positions in the gpxWaypoints array. ArrayList apList_ = new ArrayList(); ArrayList apStart_ = new ArrayList(); ArrayList apStop_ = new ArrayList(); //The first point will always be a new access point apList_.Add(gpxWaypoints[0]); apStart_.Add(0); //Lists representing the best RSSI for a unique access point and the index in the gpwWaypoints array at which it is measured ArrayList apBestSignal = new ArrayList(); ArrayList apBestSignalIdx = new ArrayList(); //The first point is the strongest (and only) point seen so far. int bestSignal = gpxWaypoints[0].extension.RSSI; apBestSignal.Add(bestSignal); apBestSignalIdx.Add(0); for (int j = 1; j < dataPoints; j++) { //If this waypoint has a different mac to the previous waypoint, then this is a new access point (access points are stored in order in the array) if (gpxWaypoints[j].extension.MAC != gpxWaypoints[j - 1].extension.MAC) { //the previous access point's block in gpxWaypoints finished with the last node apStop_.Add(j - 1); //the new access point starts here apStart_.Add(j); //Add this new waypoint. apList_.Add(gpxWaypoints[j]); //The first point is the strongest (and only) point seen so far. bestSignal = gpxWaypoints[j].extension.RSSI; apBestSignal.Add(bestSignal); apBestSignalIdx.Add(j); } //If the MAC address of this point is the same as the last. else { //If the signal is the strongest we've seen so far. if (gpxWaypoints[j].extension.RSSI > bestSignal) { bestSignal = gpxWaypoints[j].extension.RSSI; apBestSignalIdx[apList_.Count - 1] = j; apBestSignal[apList_.Count - 1] = bestSignal; } } } //the last access point will always finish where the array does. apStop_.Add((dataPoints - 1)); //From now on arrays are used rather than lists. (Array.sort function is handy) apList = (gpxWaypoint[])apList_.ToArray(typeof(gpxWaypoint)); apStart = (int[])apStart_.ToArray(typeof(int)); apStop = (int[])apStop_.ToArray(typeof(int)); //Initialise array of SSIDs. This will be used to sort the Access Point arrays in alphabetical order. SSID = new string[apList.Length]; //For every unique access point use the strongest waypoint as the gpxWaypoint stored in the apList array. for (int i = 0; i < apList.Length; i++) { int idx = (int)apBestSignalIdx[i]; gpxWaypoint tempGPXpoint = ((gpxWaypoint)apList[i]); // Use the details of the strongest signal point as the information for the access point tempGPXpoint.latitude = gpxWaypoints[idx].latitude; tempGPXpoint.longitude = gpxWaypoints[idx].longitude; tempGPXpoint.elevation = gpxWaypoints[idx].elevation; //tempGPXpoint.desc = gpxWaypoints[idx].desc; tempGPXpoint.fix = gpxWaypoints[idx].fix; tempGPXpoint.cmt = gpxWaypoints[idx].cmt; tempGPXpoint.horizontalDilution = gpxWaypoints[idx].horizontalDilution; tempGPXpoint.positionDilution = gpxWaypoints[idx].positionDilution; tempGPXpoint.numSatellites = gpxWaypoints[idx].numSatellites; tempGPXpoint.time = gpxWaypoints[idx].time; tempGPXpoint.verticalDilution = gpxWaypoints[idx].verticalDilution; tempGPXpoint.extension.RSSI = gpxWaypoints[idx].extension.RSSI; tempGPXpoint.extension.signalQuality = gpxWaypoints[idx].extension.signalQuality; tempGPXpoint.extension.privacy = gpxWaypoints[idx].extension.privacy; tempGPXpoint.extension.ChannelID = gpxWaypoints[idx].extension.ChannelID; tempGPXpoint.extension.networkType = gpxWaypoints[idx].extension.networkType; tempGPXpoint.extension.rates = gpxWaypoints[idx].extension.rates; //Store the newly updated point. apList[i] = tempGPXpoint; //Now extract the values for display in the dataview accessPointMeasurement tempPoint = tempGPXpoint.extension; //Keeps a running tally of the number of access points using a given channel. //But only if channel is above 0 //NOTE: If channel is 0 an IndexOutOfRangeException is thrown because ChannelID(is a uint) - 1 becomes 4294967295 if (tempPoint.ChannelID > 0) { channelCounts[tempPoint.ChannelID - 1]++; } //Keeps a running tally of the number of access points using a given type of encryption. encryptionCounts[getEncryptionIdx(tempPoint.privacy)]++; } } else { //The file might be an empty GPX, or an XML file that isn't in GPX format. MessageBox.Show("This file doesn't appear to have any data"); } } } #endregion #region UI Functions private void updateComprehensiveFileOptions() { organizeByLabel.Enabled = filesComprehensiveFileCheckBox.Checked; FilesComprehensiveFileComboBox.Enabled = filesComprehensiveFileCheckBox.Checked; } /// /// Enables/disables the Export button based on status of export options /// private void updateExportButtonStatus() { exportButton.Enabled = inputFileTextBox.Text.Length > 0 && exportFolderTextBox.Text.Length > 0 && (filesSummaryCheckBox.Checked || filesIndividualCheckBox.Checked || filesComprehensiveFileCheckBox.Checked); } private void SaveSettings() { //Save all the latest chosen options as defaults for next time around. //File options Settings.Default.export_inputFile = inputFileTextBox.Text; Settings.Default.export_filesFolder = exportFolderTextBox.Text; //Settings.Default.export_FilesCombine = filesCombineCheckBox.Checked; Settings.Default.export_FilesSummary = filesSummaryCheckBox.Checked; Settings.Default.export_FilesIndividualAPs = filesIndividualCheckBox.Checked; Settings.Default.export_FilesComprehensive = filesComprehensiveFileCheckBox.Checked; Settings.Default.export_FilesOrganiseByIdx = FilesComprehensiveFileComboBox.SelectedIndex; //Visibility Options //Settings.Default.export_VisAltitudeIdx = visAltComboBox.SelectedIndex; //Settings.Default.export_VisMarkerVisibility = visVisibleCheckBox.Checked; //Settings.Default.export_VisExtendedToGround = visExtrudeCheckBox.Checked; Settings.Default.export_VisShowLabels = visLabelsCheckBox.Checked; //Settings.Default.export_VisWPATKIPColorRed = visWPAColorCheckBox.Checked; //Settings.Default.export_VisIncludeTimestamp = visTimestampCheckBox.Checked; //Data Quality Options Settings.Default.export_DQLockedUp = dqGPSLockedUpCheckBox.Checked; Settings.Default.export_DQFixLost = dqSatFixLostCheckBox.Checked; Settings.Default.export_DQSatLessThan = dqSatCountCheckBox.Checked; Settings.Default.export_DQSatLessThanCount = (int)dqSatCountUpDown.Value; Settings.Default.export_DQTooFast = dqSpeedCheckBox.Checked; Settings.Default.export_DQTooFastSpeed = (int)dqSpeedUpDown.Value; Settings.Default.export_DQTooStrong = dqRSSICheckBox.Checked; Settings.Default.export_DQTooStrongStrength = (int)dqRSSIUpDown.Value; } #endregion #region Export Functions private void ExportKml() { //If there is at least one piece of data, and there's an output path to put it to if (dataPoints > 0 && exportFolderTextBox.Text.Length > 0) { //Start the timer DateTime startTime = DateTime.Now; DateTime stopTime = DateTime.Now; TimeSpan executionTime = stopTime - startTime; //These global variables are used to allow cross-thread reading of the properties of these GUI controls. //visAltComboBox_SelectedIndex = visAltComboBox.SelectedIndex; filesSummaryComboBox_SelectedIndex = FilesComprehensiveFileComboBox.SelectedIndex; showLabels = visLabelsCheckBox.Checked; //isTkipRed = visWPAColorCheckBox.Checked; //If the user didn't use a slash at the end of the path, then add one if (exportFolder[exportFolder.Length - 1] != '\\') { exportFolder += @"\"; } #if !DEBUG try { #endif //Create the export directory. Directory.CreateDirectory(exportFolder); //This will be the output sub-folder for the individual access point's KMLs if that option is selected. apFolder = exportFolder + @"APs\"; //The value of ignoreWaypoint[i] defines whether gpxWaypoints[i] complies with the data quality rules specified by the user. //For example if the GPS satellite lost it's fix, then data should be discarded. ignoreWaypoint = new bool[dataPoints]; //For every access point check the measurements against the rules and set ignoreWaypoint accordingly. for (int i = 0; i < apList.Length; i++) { //An access points measurements are in blocks in the gpxWaypoints array. //Start and stop specifies where the block starts and stops inculsive. int start = (int)apStart[i]; int stop = (int)apStop[i]; //For every measurement for this access point. for (int j = start; j <= stop; j++) { //Assume the measurement is ok (should not be ignored) until some criteria says otherwise. ignoreWaypoint[j] = false; //The user wants to ignore values if the GPS device seems to have locked up if (dqGPSLockedUpCheckBox.Checked) { //Compare this measurement to all the other measurements for this access point. for (int J = j + 1; J <= stop; J++) { //If two measurements for the same access point were taken at EXACTLY the same time then something isn't right. if (gpxWaypoints[J].time.Equals(gpxWaypoints[j].time)) { ignoreWaypoint[j] = true; break; } } } //The user only wants data if the GPS had a fix if (dqSatFixLostCheckBox.Checked) { //The GPS needs a 2D lock at the very least. Usually 3 satellites. if (gpxWaypoints[j].fix != "2d" && gpxWaypoints[j].fix != "3d") { ignoreWaypoint[j] = true; } } //The user only wants to use data if at least x number of satellites were uses to calculate the position. if (dqSatCountCheckBox.Checked) { if (gpxWaypoints[j].numSatellites < dqSatCountUpDown.Value) { ignoreWaypoint[j] = true; } } //The user wants to ignore measurements taken when they were travelling too fast. //Perhaps they're guilty about exceeding the speed limit, or perhaps the signal strength measurement losses accuracy.2 if (dqSpeedCheckBox.Checked) { double tempSpeed; //I'm not sure why this uses a try/catch. the cmt field should always be present. //However if for some reason it isn't, we're covered. try { //the speed of travel in km/h tempSpeed = System.Convert.ToDouble(gpxWaypoints[j].cmt); //If too fast, ignore the point if (tempSpeed > System.Convert.ToDouble(dqSpeedUpDown.Value)) { ignoreWaypoint[j] = true; } } catch (Exception) { //If no speed data, or speed is not recognised, ignore the point. ignoreWaypoint[j] = true; } } //If the user wants to ignore suspiciously high signal strengths. //Very occasionally and transiently the signal strength of an AP might be report as incorrectly high if (dqRSSICheckBox.Checked) { if (gpxWaypoints[j].extension.RSSI > dqRSSIUpDown.Value) { ignoreWaypoint[j] = true; } } } } //Find the strongest point for each AP again, this time only using points which meet the data quality criteria set by the user. //A similar routine is done when we load the files, however now we know how many APs there are and arrays are used. //It is possible that all of an APs datapoints could be removed which could be confusing for the user, but not incorrect. int[] apBestSignalIdx = new int[apList.Length]; int[] apBestSignal = new int[apList.Length]; //Specifies whether an access point has any valid waypoints left. If it doesn't, it should be ignored. ignoreAP = new bool[apList.Length]; //Initialise the 2D array of arraylists which will contain all the access points for a given channel and encryption for (int i = 0; i < MAX_CHANNELS; i++) { for (int j = 0; j < ENCRYPTION.Length; j++) { channelAndEncryptionIdxs[i, j] = new ArrayList(); } } //For every access point for (int i = 0; i < apList.Length; i++) { //Start off with a signal level that is impossibly low. //If all an access points measurements are removed. apBestSignal[i] = int.MinValue; //If this access point has even a single valid measurement, this should change to a positive number. apBestSignalIdx[i] = -1; int start = (int)apStart[i]; int stop = (int)apStop[i]; gpxWaypoint tempGPXpoint = ((gpxWaypoint)apList[i]); //for every measurement taken of this access point... for (int j = start; j <= stop; j++) { //that's consistent with the data quality criteria selected by the user if (!ignoreWaypoint[j]) { //Check if this is the strongest signal measurement so far if (gpxWaypoints[j].extension.RSSI > apBestSignal[i]) { //if it's stronger than any points so far, save this point. apBestSignal[i] = gpxWaypoints[j].extension.RSSI; apBestSignalIdx[i] = j; } } } int idx = (int)apBestSignalIdx[i]; if (idx > -1) { // Use the details of the strongest signal point as the information for the access point tempGPXpoint.latitude = gpxWaypoints[idx].latitude; tempGPXpoint.longitude = gpxWaypoints[idx].longitude; tempGPXpoint.elevation = gpxWaypoints[idx].elevation; tempGPXpoint.fix = gpxWaypoints[idx].fix; tempGPXpoint.cmt = gpxWaypoints[idx].cmt; tempGPXpoint.horizontalDilution = gpxWaypoints[idx].horizontalDilution; tempGPXpoint.positionDilution = gpxWaypoints[idx].positionDilution; tempGPXpoint.numSatellites = gpxWaypoints[idx].numSatellites; tempGPXpoint.time = gpxWaypoints[idx].time; tempGPXpoint.verticalDilution = gpxWaypoints[idx].verticalDilution; tempGPXpoint.extension.RSSI = gpxWaypoints[idx].extension.RSSI; tempGPXpoint.extension.signalQuality = gpxWaypoints[idx].extension.signalQuality; tempGPXpoint.extension.privacy = gpxWaypoints[idx].extension.privacy; tempGPXpoint.extension.ChannelID = gpxWaypoints[idx].extension.ChannelID; tempGPXpoint.extension.networkType = gpxWaypoints[idx].extension.networkType; tempGPXpoint.extension.rates = gpxWaypoints[idx].extension.rates; apList[i] = tempGPXpoint; //This access point was valid so add it to the list of access points for its channel and encryption type. uint channelIdx = apList[i].extension.ChannelID - 1; int encryptionIdx = getEncryptionIdx(apList[i].extension.privacy); // Only add 2.4x GHz channels for now if (channelIdx < MAX_CHANNELS) { channelAndEncryptionIdxs[channelIdx, encryptionIdx].Add(i); } //This access point has at least one measurement left ignoreAP[i] = false; } else { //This access point has absolutely not points left ignoreAP[i] = true; } } //If the user wants a file which contains all information in a single document. if (filesComprehensiveFileCheckBox.Checked) { exportComprehensiveKmlFile(); } if (filesIndividualCheckBox.Checked) { //create the sub-folder to which all these files will be written System.IO.Directory.CreateDirectory(apFolder); for (int i = 0; i < apList.Length; i++) { exportAccessPointKmlFile(i); } } /*If the user has selected that they want a summary KML file. This was one of the first methods written and has not been updated. It is written quite inefficiently. However the total time is still pretty quick so it hasn't been changed. A faster approach would be to use a streamwriter to output the file on-the-fly as raw text. This method hails from a time before there was a channelAndEncryptionIdxs array which enables all nodes to be output sequentially rather than the extra overhead of treating the file as an XML document and appending nodes to the appropriate places. */ if (filesSummaryCheckBox.Checked) { exportSummaryKmlFile(); } SaveSettings(); MessageBox.Show("Export Completed", "KML Export", MessageBoxButtons.OK, MessageBoxIcon.Information); #if !DEBUG } catch (ArgumentException exp) { MessageBox.Show("Output folder could not be created.\nYour path appears to contain illegal characters."); System.Diagnostics.Debug.WriteLine(exp.Message); } catch (DirectoryNotFoundException exp) { MessageBox.Show("Output folder could not be created.\nThe directory could not be found."); System.Diagnostics.Debug.WriteLine(exp.Message); } #endif } else { if (dataPoints == 0) { MessageBox.Show("There is no data to output."); } if (exportFolderTextBox.Text.Length==0) { MessageBox.Show("You haven't entered a path for the output files."); } } } /// /// Creates a comprehensive KML file containing ALL waypoints for ALL access points /// private void exportComprehensiveKmlFile() { //This file will be written on-the-fly to the output file. StreamWriter sw = new StreamWriter(exportFolder + "Comprehensive.kml"); sw.WriteLine(@""); sw.WriteLine(""); sw.WriteLine(""); sw.WriteLine("Comprehensive Data"); //Depending on the whether the user specified to organise the folders by channel then encryption //or the other way round will define which is the inner and outer of the for-loops. //Assuming the user wants encryption folders with channel sub-folders int outer = ENCRYPTION.Length; int inner = MAX_CHANNELS; bool isChannelFirst = false; //The user has specified the other way round, channel folders with encryption sub-folders. if (filesSummaryComboBox_SelectedIndex == 1) { outer = MAX_CHANNELS; inner = ENCRYPTION.Length; isChannelFirst = true; } //Specifies whether the higher-level (outer) folder has been created yet. //Only the folders which actually contain data will be created. //So when we find the first sub-folder with data we'll have to create its parent folder first. bool outerFolderCreated = false; //for every potential folder for (int i = 0; i < outer; i++) { //This outer folder has not been created yet. outerFolderCreated = false; //for every potential sub-folder for (int j = 0; j < inner; j++) { //The list of indicies of the apList array for this encryption and channel (i.e. this sub-folder) ArrayList tempList; //The user specified hierarchy will specify whether i corresponds to encryption or channel and vice-versa for j if (isChannelFirst) { tempList = channelAndEncryptionIdxs[i, j]; } else { tempList = channelAndEncryptionIdxs[j, i]; } //If there is at least one measurement for this encryption and channel (i.e. this sub-folder) if (tempList.Count > 0) { //If the parent folder has not been created yet, do it now. if (!outerFolderCreated) { sw.WriteLine(""); //The user-specified hierarchy will define whether the parent folder is for encryption or channel if (!isChannelFirst) { sw.WriteLine("" + ENCRYPTION[i] + ""); } else { sw.WriteLine(" Channel " + (i + 1) + ""); } //remember that the parent folder has already been made. outerFolderCreated = true; } //now create the sub-folder (inner) sw.WriteLine(""); //and give it the appropriate name if (isChannelFirst) { sw.WriteLine("" + ENCRYPTION[j] + ""); } else { sw.WriteLine(" Channel " + (j + 1) + ""); } //for every access point that uses this channel and encryption type for (int k = 0; k < tempList.Count; k++) { //select the access point int I = (int)tempList[k]; gpxWaypoint currentAP = (gpxWaypoint)apList[I]; //use its name as the name of the folder. Name is {SSID} [{MAC ADDRESS}] string tempName = (currentAP.name); sw.WriteLine(""); sw.WriteLine("" + XmlCleanUp.CleanUp(tempName) + ""); //use the standard shaded dot marker. string iconLink = "http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png"; //Scale the marker depending on the signal strength double iconScale_ = getIconScale(currentAP.extension.RSSI); //run method which which writes a gpxWaypoint to a element. //The strongest point will be in the root of the AP's folder with all the individual datapoints in sub-folder below writePlacemark(currentAP, sw, iconLink, iconScale_, currentAP.extension.RSSI.ToString()); //output all the individual measurements to a sub-folder of the AP's folder exportAccessPointToKml(I, sw); //Close Access Point Folder sw.WriteLine(""); } //Close inner folder (encryption or channel) sw.WriteLine(""); } //Close outer folder (encryption or channel) if (j == (inner - 1) && outerFolderCreated) { sw.WriteLine(""); } } } //Close the KML document and the streamwriter. sw.WriteLine(""); sw.WriteLine(""); sw.Close(); } private void exportSummaryKmlFile() { //This document will contain markers at the strongest point for each access point organised by channel and encryption. XmlDocument summaryDocument = new XmlDocument(); //Using LoadXml is relatively slow, but it's not really a problem as there's only one summary document to write. summaryDocument.LoadXml(@"Summary"); //Whether encryption types are folders within channel folders or the other way round depends on what the user chose. //Start by assuming that channels are sub-folders of encryption folders. int outer = ENCRYPTION.Length; int inner = MAX_CHANNELS; bool isChannelFirst = false; //User wants the other way round, encryption is a sub-folder of channel if (FilesComprehensiveFileComboBox.SelectedIndex == 1) { outer = MAX_CHANNELS; inner = ENCRYPTION.Length; isChannelFirst = true; } //FIRST CREATE THE FOLDER STRUCTURE OF THE KML FILE //Outer folder for (int i = 0; i < outer; i++) { //Don't create a folder for it, if there's definitely no data. //This will still make a folder if the data was completely removed due to data quality settings. if ((isChannelFirst && channelCounts[i] > 0) || (!isChannelFirst && encryptionCounts[i] > 0)) { //Create a folder node and give it a name depending on what the user chose as the outer folder XmlElement Folder = summaryDocument.CreateElement("Folder"); XmlElement nameElement = summaryDocument.CreateElement("name"); if (isChannelFirst) { nameElement.InnerText = "Channel " + (i + 1) + ""; } else { nameElement.InnerText = ENCRYPTION[i]; } //This folder will be closed (not expanded) by default in google earth. XmlElement openElement = summaryDocument.CreateElement("open"); openElement.InnerText = "0"; //The visibility of this folder will be as per the users selection XmlElement visibilityElement = summaryDocument.CreateElement("visibility"); //visibilityElement.InnerText = System.Convert.ToInt32(visVisibleCheckBox.Checked) + ""; visibilityElement.InnerText = "1"; // markers are always visible by default //Append the name, open and visibility elements to the Folder. Folder.AppendChild(nameElement); Folder.AppendChild(openElement); Folder.AppendChild(visibilityElement); //Now start work on the inner folder for (int j = 0; j < inner; j++) { //Don't create a folder for it, if there's definitely no data. //This will still make a folder if the data was completely removed due to data quality settings. if ((!isChannelFirst && channelCounts[j] > 0) || (isChannelFirst && encryptionCounts[j] > 0)) { //Create the folder element and give it the appropriate name. XmlElement Sub_Folder = summaryDocument.CreateElement("Folder"); XmlElement Sub_nameElement = summaryDocument.CreateElement("name"); if (isChannelFirst) { Sub_nameElement.InnerText = ENCRYPTION[j]; //An id attribute is added to make it easier/quicker to find Sub_Folder.SetAttribute("id", (i + 1) + "," + j); } else { Sub_nameElement.InnerText = "Channel " + (j + 1) + ""; //An id attribute is added to make it easier/quicker to find Sub_Folder.SetAttribute("id", (j + 1) + "," + i); } //This folder will be closed (not expanded) by default in google earth. XmlElement Sub_openElement = summaryDocument.CreateElement("open"); Sub_openElement.InnerText = "0"; //The visibility of this folder will be as per the users selection XmlElement Sub_VisibilityElement = summaryDocument.CreateElement("visibility"); //Sub_VisibilityElement.InnerText = System.Convert.ToInt32(visVisibleCheckBox.Checked) + ""; Sub_VisibilityElement.InnerText = "1"; // markers are always visible by default //Append the name, open and visibility elements to the Folder. Sub_Folder.AppendChild(Sub_nameElement); Sub_Folder.AppendChild(Sub_openElement); Sub_Folder.AppendChild(Sub_VisibilityElement); //Append the sub-folder to its parent folder Folder.AppendChild(Sub_Folder); } } //Append the Folder to the document. summaryDocument.GetElementsByTagName("Document").Item(0).AppendChild(Folder); } } //NOW ADD THE RESPECTIVE ACCESS POINTS TO THE APPROPRIATE FOLDERS //All markers will be the default shaded-dot string iconLink = "http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png"; //for every access point for (int i = 0; i < apList.Length; i++) { //every access point with at least one measurement if (!ignoreAP[i]) { //Select the current access point gpxWaypoint currentAP = (gpxWaypoint)apList[i]; //get the color which matches it's encryption type string defaultColor = getEncryptionColor(currentAP.extension.privacy); //use this color for the icon and the label string iconColor = defaultColor; string labelColor = defaultColor; string labelScale = showLabels ? "1" : "0"; //Convert the signal strength to a marker size string iconScale = getIconScale((int)currentAP.extension.RSSI) + ""; //The main element is the placemark XmlElement Placemark = summaryDocument.CreateElement("Placemark"); //The visibility of this placemark will be as per the user specified in the appropriate checkbox XmlElement visElement = summaryDocument.CreateElement("visibility"); //visElement.InnerText = System.Convert.ToInt32(visVisibleCheckBox.Checked) + ""; visElement.InnerText = "1"; //The style element is lazily made setting the text in the innerXml field. //The style element defines things like the icon and labels sizes, colors and icon image. XmlElement styleElement = summaryDocument.CreateElement("Style"); { styleElement.InnerXml = "" + iconLink + "" + iconColor + "" + iconScale + "" + labelColor + "" + labelScale + ""; styleElement.SetAttribute("id", "sn_shaded_dot"); } //The element which defines the name of the placemark is the name of the AP {SSID} [{MAC ADDRESS}] XmlElement nameElement = summaryDocument.CreateElement("name"); nameElement.InnerText = (currentAP.name); //The description of the placemark as generated previously in the LogViewer XmlElement descElement = summaryDocument.CreateElement("description"); descElement.InnerText = (currentAP.description); //This element defines the GPS coordinates, altitude and method in which altitude is shown. XmlElement pointElement = summaryDocument.CreateElement("Point"); //element with the GPS information XmlElement coordElement = summaryDocument.CreateElement("coordinates"); //In the summary document, the elevation is always shown as the real elevation rather than a representation of the signal strength for example. coordElement.InnerText = currentAP.longitude + "," + currentAP.latitude + "," + currentAP.elevation; //Add the GPS coordinates to the point element pointElement.AppendChild(coordElement); //Add all the elements to the placemark Placemark.AppendChild(visElement); Placemark.AppendChild(styleElement); Placemark.AppendChild(nameElement); Placemark.AppendChild(descElement); Placemark.AppendChild(pointElement); int encryptionType = getEncryptionIdx(currentAP.extension.privacy); //the xpath to the appropriate channel/encryption folder. string xpath = "//Folder[@id='" + currentAP.extension.ChannelID + "," + encryptionType + "']"; //Append the placemark to the current folder XmlNode node = summaryDocument.SelectSingleNode(xpath); if (null != node) { node.AppendChild(Placemark); } else { Console.WriteLine("ERROR: Node not found: " + xpath); } } } //Save the completed file. summaryDocument.Save(exportFolder + "Summary.kml"); } /// /// Outputs the data of access point apList[e] to file. /// /// index of apList to be processed. private void exportAccessPointKmlFile(int i) { //This will be access point being processed. For now we just need to work out the filename to use. gpxWaypoint currentAP = (gpxWaypoint)apList[i]; // Make the AP name a legal file name char[] badChars = Path.GetInvalidFileNameChars(); String tempName = String.Join ("", currentAP.name.Split(badChars,StringSplitOptions.RemoveEmptyEntries)); // if the AP name will result in a fully qualified filename that is too long... if (tempName.Length > 255 - apFolder.Length) { tempName = tempName.Remove( 255 - apFolder.Length ); int count = 0; while (File.Exists(apFolder + tempName + ".kml")) { string countString = count.ToString(); tempName = tempName.Remove(tempName.Length - countString.Length) + countString; count++; } } //Our output file will be at this location. string filename2 = apFolder + tempName + ".kml"; //The file is output on-the-fly by the streamwriter to the file at filename2 StreamWriter sw = new StreamWriter(filename2); //Open the root nodes of the document sw.WriteLine(@""); sw.WriteLine(""); sw.WriteLine(""); //given the index of the access point and a streamwriter to use, this will write all the placemarks and sub-folders for the access point as per the options selected by the user. exportAccessPointToKml(i, sw); //Close the root nodes of the document and the streamwriter itself. sw.WriteLine(""); sw.WriteLine(""); sw.Close(); //Every file processed is one less thread that the ThreadPool has to worry about. if (Interlocked.Decrement(ref numBusy) == 0) { //When no files remain, the thread can stop waiting and will terminate. resetEvents[0].Set(); } } /// /// Writes the data for a given access point at the specified index to the specified streamwriter. /// /// index of apList to be processed. /// streamwriter used to write the data. private void exportAccessPointToKml(int i, StreamWriter sw) { //This is the access point to be processed. gpxWaypoint currentAP = (gpxWaypoint)apList[i]; //the measurements for an access point occupy contigious blocks in the gpxWaypoints array. int startIndex = (int)apStart[i]; int stopIndex = (int)apStop[i]; //create the folder sw.WriteLine(""); //and give it a name depending on which folder we are currently processing. sw.WriteLine("RSSI"); //for every measurement belonging to this access point. for (int j = startIndex; j <= stopIndex; j++) { gpxWaypoint tempAP = gpxWaypoints[j]; /*If this is a measurement that meets the user specified data quality criteria * and double checks that this waypoint/measurement does in fact belong to the right AP. * That check is redundant but was useful whilst debugging. */ if ((currentAP.extension.MAC == tempAP.extension.MAC) && (ignoreWaypoint[j] == false)) { //The strength of the current measurement in dBm. int tempStrength = gpxWaypoints[j].extension.RSSI; //Marker Size is related to signal strength, unless signal strength is already related to elevation double iconScale_ = getIconScale(tempStrength); string iconLink, name; //If this point is the strongest or equal strongest, use the paddle icon instead of the shaded dot. if (tempStrength == currentAP.extension.RSSI) { iconLink = "http://maps.google.com/mapfiles/kml/paddle/wht-blank.png"; name = gpxWaypoints[j].name + ": " + tempStrength.ToString(); } else { iconLink = "http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png"; name = tempStrength.ToString(); } //use the options generated so far and the streamwriter to write the tempAP gpxWaypoint. writePlacemark(tempAP, sw, iconLink, iconScale_, name); } } //Completed all measurements for this access point so close the folder. sw.WriteLine(""); } /// /// Writes the placemark element for a specified waypoint using the specified streamwriter. /// /// Waypoint to be processed /// streamwriter used to write the data. /// Link to the marker to be used. /// Size of the marker. /// Should the marker be visible by default. /// Should a line connect the marker to ground level. /// Switch to indicate how to represent the altitude of the point. private void writePlacemark(gpxWaypoint AP, StreamWriter sw, string iconLink, double iconScale_, string placemarkName) { //The color which corresponds to this points encryption type string defaultColor = getEncryptionColor(AP.extension.privacy); string iconColor = defaultColor; string labelColor = defaultColor; //If the user wants labels, give them labels. string labelScale = showLabels ? "1" : "0"; //The size of the icon is passed into this method and converted to a string here. string iconScale = iconScale_.ToString(); //Open the Placemark element sw.WriteLine(""); { //define the default visiblity of this placemark sw.WriteLine("1"); //write the style element as per the specifications input by the user. sw.WriteLine(@""); //The name of the placemark is it's signal strength in dBm. sw.WriteLine("" + placemarkName + ""); //This is the description of the waypoint as created by the LogViewer previously. sw.WriteLine("" + AP.description + ""); //The Point element contains the GPS information. sw.WriteLine(""); // The coordinates: latitude, longitude, and elevation sw.WriteLine("" + AP.longitude + "," + AP.latitude + "," + AP.elevation + ""); //Close the Point tag. sw.WriteLine(""); } //Close the Placemark tag. sw.WriteLine(""); } /// /// Converts a string description of the encryption type to an index representing that encryption type. /// /// Encryption type description. private int getEncryptionIdx(string desc) { if (desc.Contains("No") || desc.Contains("no")) return 1; if (desc.Contains("WEP")) return 2; if (desc.Contains("WPA-TKIP")) return 4; if (desc.Contains("WPA2") || desc.Contains("RSNA")) return 5; if (desc.Contains("WPA")) return 3; //If its made it this far I don't know what it is? return 0; } /// /// Converts a string description of the encryption type to a color code string for the KML file. /// /// Encryption type description. private string getEncryptionColor(string input) { int encryptionType = getEncryptionIdx(input); if (encryptionType == 1) { //GREEN return "ff00ff00"; } if (encryptionType == 2) { //YELLOW return "ff00ffff"; } //WPA-TKIP sometimes means the AP is operating in mixed WPA-WPA2 mode. The user can define whether it wants to use the WPA or WPA2 coloring. if ((encryptionType == 3) || (encryptionType == 4 && !isTkipRed)) { //ORANGE return "ff00aaff"; } //WPA-TKIP sometimes means the AP is operating in mixed WPA-WPA2 mode. The user can define whether it wants to use the WPA or WPA2 coloring. if ((encryptionType == 5) || (encryptionType == 4 && isTkipRed)) { //RED return "ff0000ff"; } //If its made it this far I don't know what it is? return "ffffffff"; } /// /// Converts an RSSI dBm value to a marker scale size /// /// The signal strength in dBm private double getIconScale(int RSSI) { return ((100 + RSSI) / 15.0); } #endregion #region Event Handlers /// /// Loads the last-used settings /// /// /// private void KmlExporterForm_Load(object sender, EventArgs e) { //File options // input GPX file inputFileTextBox.Text = Settings.Default.export_inputFile; //The output folder for the processed files exportFolderTextBox.Text = Settings.Default.export_filesFolder; //Does the user want to combine all the valid GPX waypoints to a single GPX file? //filesCombineCheckBox.Checked = Settings.Default.export_FilesCombine; //Does the user want a summary KML which contains only the strongest points for each AP filesSummaryCheckBox.Checked = Settings.Default.export_FilesSummary; //Does the user want individual KML files for every AP containing all the valid measurements for that AP. filesIndividualCheckBox.Checked = Settings.Default.export_FilesIndividualAPs; //Does the user want to output all the data to a single KML file filesComprehensiveFileCheckBox.Checked = Settings.Default.export_FilesComprehensive; //Defines the hierachy or the KML files. Channel then encryption or the otherway round. FilesComprehensiveFileComboBox.SelectedIndex = Settings.Default.export_FilesOrganiseByIdx; //Visibility Options //How should altitude be output in the KML files? As signal strength, height, speed etc.? //visAltComboBox.SelectedIndex = Settings.Default.export_VisAltitudeIdx; //Should the markers be visible by default? The main marker for each AP in the comprehensive KML file will always be visible. //visVisibleCheckBox.Checked = Settings.Default.export_VisMarkerVisibility; //Should there be a marker that joins the marker to it's position on the ground. //Again, the main marker always has this in the comprehensive KML file. //visExtrudeCheckBox.Checked = Settings.Default.export_VisExtendedToGround; //Should markers have their labels visible? visLabelsCheckBox.Checked = Settings.Default.export_VisShowLabels; //Should WPA-TKIP points be colored red or orange? //visWPAColorCheckBox.Checked = Settings.Default.export_VisWPATKIPColorRed; //Should the KML file contain the timestamp node. The time will be visible in the description, however inclusion //of the timestamp field will allow animation of the data or to compare values taken on two different occasions. //It can also make viewing the data in google earth confusing. //visTimestampCheckBox.Checked = Settings.Default.export_VisIncludeTimestamp; //Data Quality Options //Should points be discarded if the GPS has locked up? i.e. if an access point has more than one measurement for exactly the same time. dqGPSLockedUpCheckBox.Checked = Settings.Default.export_DQLockedUp; //Should points be discarded if there was no valid GPS fix when the data was recorded? dqSatFixLostCheckBox.Checked = Settings.Default.export_DQFixLost; //Should points be discarded if the GPS fix was using less than or equal to this number of satellites dqSatCountCheckBox.Checked = Settings.Default.export_DQSatLessThan; dqSatCountUpDown.Value = Settings.Default.export_DQSatLessThanCount; //Should points be discarded if you were travelling too fast. dqSpeedCheckBox.Checked = Settings.Default.export_DQTooFast; dqSpeedUpDown.Value = Settings.Default.export_DQTooFastSpeed; //Should points be discarded if they appear to be too strong? //Sometimes a very strong value will be read very briefly which is not correct. dqRSSICheckBox.Checked = Settings.Default.export_DQTooStrong; dqRSSIUpDown.Value = Settings.Default.export_DQTooStrongStrength; } private void filesComprehensiveFileCheckBox_CheckedChanged(object sender, EventArgs e) { updateComprehensiveFileOptions(); updateExportButtonStatus(); } /// /// Launches a FileDialog to select the input file(s). /// private void selectInputFileButton_Click(object sender, EventArgs e) { //Launch the dialog box to prompt for files. if (selectInputFileDialog.ShowDialog() == DialogResult.OK) { // Display the input file name inputFileTextBox.Text = selectInputFileDialog.FileName; } updateExportButtonStatus(); } /// /// Launches a FolderBrowserDialog to select the output folder. /// /// /// private void selectExportFolderButton_Click(object sender, EventArgs e) { if (selectExportFolderDialog.ShowDialog() == DialogResult.OK) { exportFolderTextBox.Text = selectExportFolderDialog.SelectedPath; } updateExportButtonStatus(); } private void ExportButton_Click(object sender, EventArgs e) { fileNames[0] = inputFileTextBox.Text; exportFolder = exportFolderTextBox.Text; loadInputFiles(); ExportKml(); } #endregion private void filesSummaryCheckBox_CheckedChanged(object sender, EventArgs e) { updateExportButtonStatus(); } private void filesIndividualCheckBox_CheckedChanged(object sender, EventArgs e) { updateExportButtonStatus(); } } }