public void createBackgroundMaps(ref MissionSummary msn, FlightlineSummary[] FLSum, String UTMZone, bool downloadMap) { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //create the background map that will be used for Waldo_FCS //also use this map as an icon when selecting the mission from the mission selection screen //procedure::::::: //get the bounding rectangle for the set of flight lines for each mission //establish a border around this bounding box based on the percent //between the bounding box extents -- currently set to minimum of 25% on each side (NS or EW) //next calculate the GE Maps zoom level that will establish the Google Earth minimum-size map to encompass this area //we initialize at zoom = 7 (large map) and increase until the map coverage doesnt encompass the desired extent //we always assume a 640 X 480 Google Earth map because thats the largest size that is free. //Google Maps for Business allows larger maps!!! //We determine the NW and SE corners of the downloaded Google Earth map for use in flight management //We download the map using the Google Map API (just pass a URL to a .net web client) //Next we draw the flight lines on this map using the .net drawing tools //the store the map to a BackgroundImages folder for later use by the GeoScanner. //in another location of this code, we write the .kml to allow the background map to be viewed by the mission planner kml //you can bundle the .kml file and the BackgroundImages folder into a zip and rename to kmz for a consolidated mission plan /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// double maxLat = -999.0, maxLon = -999.0, minLat = 999.0, minLon = 999.0; double maxUTMX = -99999999999.0; double maxUTMY = -99999999999.0; double minUTMX = 99999999999.0; double minUTMY = 99999999999.0; //get the bounding box for the mission polygons (formed by ends of the FLs) //extend the flight lines by 4 mi at each end. double extendDistanceMet = 4.0 * 5280.0 * 0.3048; // 4 miles in meters for (int j = msn.startFlightLine; j < (msn.startFlightLine + msn.numberFlightlines); j++) { //form a unit vector along each flight line -- start-to-end double delX = FLSum[j].planned.endUTMX - FLSum[j].planned.startUTMX; double delY = FLSum[j].planned.endUTMY - FLSum[j].planned.startUTMY; double mag = Math.Sqrt(delX * delX + delY * delY); delX /= mag; delY /= mag; //project 4 mi beyond the end double extendedEndX = FLSum[j].planned.endUTMX + delX * extendDistanceMet; double extendedEndY = FLSum[j].planned.endUTMY + delY * extendDistanceMet; double extendedStartX = FLSum[j].planned.startUTMX - delX * extendDistanceMet; double extendedStartY = FLSum[j].planned.startUTMY - delY * extendDistanceMet; if (extendedEndX > maxUTMX) maxUTMX = extendedEndX; if (extendedEndY > maxUTMY) maxUTMY = extendedEndY; if (extendedStartX > maxUTMX) maxUTMX = extendedStartX; if (extendedStartY > maxUTMY) maxUTMY = extendedStartY; if (extendedEndX < minUTMX) minUTMX = extendedEndX; if (extendedEndY < minUTMY) minUTMY = extendedEndY; if (extendedStartX < minUTMX) minUTMX = extendedStartX; if (extendedStartY < minUTMY) minUTMY = extendedStartY; } utm.UTMtoLL(maxUTMY, minUTMX, UTMZone, ref maxLat, ref minLon); utm.UTMtoLL(minUTMY, maxUTMX, UTMZone, ref minLat, ref maxLon); //compute the desired map center from the lat/lon max and min PointD mapCenter = new PointD((maxLon + minLon) / 2.0, (maxLat + minLat) / 2.0); //set a buffer around the map size that will contain the mission flight lines plus a border //make this buffer a fixed size (rather than percent) to allow for very small bounding boxes double bufferPercent = 0.00; //this bufgfer has been removed and replaced with the end-line extensions double latBuffer = bufferPercent * (maxLat - minLat); double lonBuffer = bufferPercent * (maxLon - minLon); // get the google maps zoom level that will encompass this map within a 640X480 pixel size int selectedZoom = 1; //initial guess at the zoom //note: the returned image coverage/size is reduced as the zooom is increased //so increase til the the image no longer covers the bounding box -- image size assumed to be 640X480 //640 X 480 is largest image that can be retreived frn Google Maps for Free. for (int zoom = 7; zoom < 20; zoom++) { setMap(zoom, mapCenter); //gets the map bounding box given mapcenter and zoom level PointD mapNWCordinates = getNWMapCoordinates(); //all we need to test is one corner -- coverage reduces about the center //not sure about this!! -- may need to check all the corners!! if (mapNWCordinates.X > (minLon - lonBuffer) || mapNWCordinates.Y < (maxLat + latBuffer)) { //test to see if the desired boundary rect is no longer contained in the Google Maps image //set the desired zoom level to the last good zoom level selectedZoom = zoom - 1; break; } } //this URL returns a valid image //http://maps.googleapis.com/maps/api/staticmap?center=40.714728,-73.998672&zoom=12&size=400x400&sensor=false //build up the URL to retrieve the Google Map -- see the above model for the syntax String GoogleMapsURL = "http://maps.googleapis.com/maps/api/staticmap?center=" + mapCenter.Y.ToString() + "," + mapCenter.X.ToString() + "&zoom=" + selectedZoom.ToString() + "&size=640x480&sensor=false"; //set the file storage location String backgroundImageFolder = datasetFolder + "\\" + polygonName + "_Background"; if (!Directory.Exists(backgroundImageFolder)) Directory.CreateDirectory(backgroundImageFolder); String SaveToFile = backgroundImageFolder + "\\Background_" + msn.missionNumber.ToString("D2") + ".png"; //enforce the Google API Maps interface to make requests slower than 10 requests/second (See Google API Service Agreement) //measure elapsed time since the last request int elapsedMillisecs = (DateTime.Now - timeSinceLastMapRequest).Milliseconds; //elapsed time since last request //if time between this and last request is too fast, then Sleep //below we ensure we dont Make requests faster than 2/sec -- 0.5 secs between requests if (elapsedMillisecs < 500) //if elapsed time < 0.5 secs sleep long enough to make the elapsed time 0.5 secs { Thread.Sleep(500 - elapsedMillisecs); } timeSinceLastMapRequest = DateTime.Now; //get the file from the web using a webClient if (downloadMap) { getFileFromWeb(GoogleMapsURL, SaveToFile); } setMap(selectedZoom, mapCenter); msn.backgroundImageNWCorner = getNWMapCoordinates(); msn.backgroundImageSECorner = getSEMapCoordinates(); msn.backgroundImageFilename = polygonName + "_Background\\Background_" + msn.missionNumber.ToString("D2") + ".png"; ///////////////////////////////////////////////////////////////////////////////// //below material generates a map presentation with the flight lines overlayed ///////////////////////////////////////////////////////////////////////////////// //Image img = Image.FromFile(SaveToFile); //get an image object from the stored file ////must convert this image into a non-indexed image in order to draw on it -- saved file PixelFormat is "Format8bppindexed" //Bitmap bm = new Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb); //Graphics g = Graphics.FromImage(bm); //create a graphics object //g.DrawImage(img, 0, 0); //now draw the original indexed image (from the file) to the non-indexed image //for (int j = msn.startFlightLine; j < (msn.startFlightLine + msn.numberFlightlines); j++) //{ // Point startFLPix = FromCoordinatesToLocalMapPixels(new PointD(FLSum[j].planned.startLon, FLSum[j].planned.startLat)); //first convert center to Google Map pixels // Point endFLPix = FromCoordinatesToLocalMapPixels(new PointD(FLSum[j].planned.endLon, FLSum[j].planned.endLat)); //first convert center to Google Map pixels // g.DrawLine(new Pen(Color.Red, 3), startFLPix, endFLPix); //} //String missionImageFolder = datasetFolder + "\\" + polygonName + "_Summary"; //if (!Directory.Exists(missionImageFolder)) Directory.CreateDirectory(missionImageFolder); //bm.Save(datasetFolder + "\\" + msn.backgroundImageFilename); }
////////////////////////////////////// //all the action occurs here ////////////////////////////////////// private void prepareKML() { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // this procedure is called immediately after we have clicked the BROWSE button and selected the input file ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// //filestream to create the files where we will write some summary output (debug: KMLDebugFile) FileStream fs1; try { //debug output data fs1 = new FileStream(datasetFolder + "\\KMLDebugFile.txt", FileMode.Create, FileAccess.Write, FileShare.None); outfile = new StreamWriter(fs1); outfile.AutoFlush = true; } catch { MessageBox.Show("Cant open the KMLDebugFile file "); } //Constants.Log -- writes the the RTF on the main page of the GUI Constants.Log("File opened for writing log information: \n" + datasetFolder + "\\KMLDebugFile.txt"); //First determine whether the mission is a linear or area mission //In Google Earth -- use the Polygon tool for the polygon and the Path tool for the linear feature //can detect these by inspecting the input KML //Path has only a LineString tag and coordonate tag //Polygon has a Polygon tag, OuterBoundaryIs tag, linearRing tag, coordinates tag //get the lat/lon client data and other info from the input kml file InspectKMLforMissions kml = new InspectKMLforMissions(kmlInputFilename, datasetFolder, outfile); //generate polygon or linearFeature geometry information and further populate the job definition structure //convert geodetic to UTM coordinates, compute area, bounding box, etc ... COVERAGE_TYPE polyCoverageType_polygonOrLinear = kml.polygonGeometry(); //expand the poly boundary for insurance -- destroying the original polygon if (polyCoverageType_polygonOrLinear == COVERAGE_TYPE.polygon) { //original lat-lon poonts saved in originalLatitudeDeg, originalLongitudeDeg within poly description double borderWidth = 10; //probably should be adaptive to 1/2 the swath width !! kml.expandPolygonVertices(borderWidth); //expand the client poly vertices with a border for insurance } //get the airports that are close to the Job polygons kml.getAirports(); //////////////////////////////////////////////////////////////////////////////////////////////////////////// //We plan to allow three levels of naming: job, project, polygon //the original idea was that job = the overall job containing projects and individual polygons in a job //an example would be //job: agricultureFieldsForDate //Project: FarmerJohn, FarmerJoe //polygon: field#1, field#2, etc //However, the current setup is that we have a single polygon in the input kml file //we will use the polygon.name for the output kml file --- that becomes the flight plan for the flight computer ///////////////////////////////////////////////////////////////////////////////////////////////////////////// //retrieve a local copy of the Project Polygons from the client-input kml //shared for the polygon and linear feature coverage polys = new List<Polygon>(); //do we need this statement ??? polys = kml.getPolygonsFromKML(); //must see if the input kml file and the polygon.name are the same. //if polygon name and inut polygon name are the same, the input file will be deastroyed (over-written) //if names are the same: add "_input" to the original name DirectoryInfo kmlInfo = new DirectoryInfo(kmlInputFilename); String inputFilenameWOextension = Path.GetFileNameWithoutExtension(kmlInputFilename); if (inputFilenameWOextension == polys[0].PolygonName) { MessageBox.Show("Google Earth placemark name same as input kml filename. \n" + "Input kml name changed by appending _input at the end"); String newName = Path.GetDirectoryName(kmlInputFilename) + "\\" + inputFilenameWOextension + "_input.kml"; System.IO.File.Move(kmlInputFilename, newName); Constants.Log(""); Constants.Log("The input kml fileName and the Google Earth Placemark name are the same"); Constants.Log("The input kml filename has been changed by adding _input to its fileName"); Constants.Log("The prepared mission plan will have the name of the original kml file"); Constants.Log("Use the new kml filename (with _input) on any new flight planning exercise"); } //////////////////////////////////////////////////////////////////////////////////// //at this point, we have the polygon selected as well as the nearby airports //////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////// //Cycle through all the Project Polygons // for now -- we only allow a single poly at a time //however, the kml read program allows mpre than one at a time //this would be useful for clients with a lot of polygon areas //////////////////////////////////////////////////////////////// //for (int totIt=0; totIt<4; totIt++) { //disable the browse button after the input kml selection has been completed button1.Visible = false; button1.Enabled = false; Polygon p = polys[0]; //we need to display the airports and allow the user to select a project airport. //show the airport list in a datGridView and wait for the user to select the correct airport. foreach (airport apt in p.airports) this.dataGridView1.Rows.Add(apt.name, apt.distanceToPolyCenterMi.ToString("F1")); Constants.Log(" "); Constants.Log("Please select a takeoff airport from the dropdown list "); this.dataGridView1.Enabled = true; this.dataGridView1.Visible = true; this.dataGridView1.CurrentCell.Selected = false; this.label2.Enabled = true; this.label2.Visible = true; //wait here while the user selects the takeoff airport //the selected airport is given: selectedAirportIndex while (!airportSelected) { Application.DoEvents(); } Constants.Log("Selected airport: " + p.airports[selectedAirportIndex].name); Constants.Log(" "); Constants.Log("Please select a camera .... "); this.listBox2.Enabled = true; this.listBox2.Visible = true; this.label1.Enabled = true; this.label1.Visible = true; //wait here for the user to select a camera while (!cameraSelected) { Application.DoEvents(); //wait for user inputs } String cameraType = this.listBox2.SelectedItem.ToString(); Constants.Log(" "); Constants.Log("Please select a resolution (cm) .... "); this.listBox3.Enabled = true; this.listBox3.Visible = true; this.label3.Enabled = true; this.label3.Visible = true; //wait here for the user to select a resolution while (!ResolutionSelected) { Application.DoEvents(); //wait for user inputs } //no need to select max flight line for a linear feature path coverage if (p.polyCoverageType == COVERAGE_TYPE.polygon) { Constants.Log(" "); Constants.Log("Please select a max Flightline Length (nm) .... "); this.listBox4.Enabled = true; this.listBox4.Visible = true; this.label4.Enabled = true; this.label4.Visible = true; //wait here for the user to select a max flight line length while (!maxFLSelected) { Application.DoEvents(); //wait for user inputs } //no need to select max mission time for a linear feature path coverage //assume the user will make the linear features of flyable lengths Constants.Log(" "); Constants.Log("Large coverages may require multiple flight missions. "); Constants.Log("Please select a maximum per-mission ferry+overtarget flight time (hrs) .... "); this.listBox6.Enabled = true; this.listBox6.Visible = true; this.label6.Enabled = true; this.label6.Visible = true; //wait here for the user to select a max mission time while (!maxMissionTimeSelected) { Application.DoEvents(); //wait for user inputs } } else { this.panel1.Enabled = true; this.panel1.Visible = true; radioButton1.Checked = false; radioButton2.Checked = false; radioButton3.Checked = false; //wait here for the user to select the number of prlallel paths while (!linearFeaturePathsSelected || (radioButton1.Checked == false && radioButton2.Checked == false && radioButton3.Checked == false) ) { Application.DoEvents(); //wait for user inputs } if (radioButton1.Checked == true) {linearPathCentering = LINEAR_PATH_CENTERING.centeredOnInputPath; } else if (radioButton2.Checked == true) {linearPathCentering = LINEAR_PATH_CENTERING.offsetToLeftOfInputPath; } else if (radioButton3.Checked == true) {linearPathCentering = LINEAR_PATH_CENTERING.offsetToRightOfInputPath; } } Constants.Log(" "); Constants.Log("Please select a nominal aircraft speed (knots) .... "); this.listBox5.Enabled = true; this.listBox5.Visible = true; this.label5.Enabled = true; this.label5.Visible = true; //wait here for the user to select a aircraft speed while (!nominalSpeedSelected) { Application.DoEvents(); //wait for user inputs } p.selectedAirportIndex = selectedAirportIndex; ////////////////////////////////////////////////////////////////////////// //at this point, we have completed all the user's initial inputs. //next we compute the performance for the three coverage types ////////////////SETUP PARAMETERS/////////////////////////////////////////////////////////////////// /// specific setup for the Canon 5D rotated 90 deg (long side downrange) //double flightAlt = 3500.0; //5,000 provides about 6" resolution -- !!!!!!!!!!this is assumed to be agl!!!!!!!!!!! //compute the flight line spacing from the camera FOV and altitude double CRFOV = 27.5; //degrees crossrange FOV (rotated 5D with 50m lenses -- GeoPod) double DRFOV = 39.0; //degrees downrange FOV (rotated 5D with 50mm lens) double downlap = 60; //percent downlap X 100 -- 60 double sideLap = 30.0; //percent sidelap X 100 int cameraPixels = 3744; //pixels in crossrange (rotated canon 5D) //max FL length is in nautical miles double maxFLLengthMi = maxFLLengthNM * 1.15078; //convert nautical miles to statute miles double timeToTurn = 1.0 / 60.0; //hours -- about 2 min -- this is conservative 1.5 is typical double aircraftSpeedKnots = nominalSpeed; // 100.0; double takeoffTime = 5.0 / 60.0; //time to roll/run-up on the runway turing the takeoff event -- added to the ferry time double landingTime = 5.0 / 60.0; //time in pattern & taxi roll on the runway turing the landing event -- added to the ferry time //we attempt to merge small lines (below minLineLength) with prior lines double minLineLength = 8.0; //minimum length of a flight line segment (units km) /////////////////////////////////////////////////////////////////////////////////////////////////////////// //derived setup parameters///////////////////////////////////////////////////////////////////////////////////////////////// //double swathWidth = 2.0 * Math.Tan(CRFOV * Constants.Deg2Rad / 2.0) * flightAlt; //in feet //double GSD = swathWidth / cameraPixels * 0.3048; //GSD im meters double swathWidthMeters = resolutionCm * cameraPixels / 100.0; double flightAlt = swathWidthMeters / (2.0 * Math.Tan(CRFOV * Constants.Deg2Rad / 2.0)) / 0.3048; double swathWidthWithSidelap = (1.0 - sideLap / 100.0) * swathWidthMeters; //in meters double swathWidthKm = swathWidthWithSidelap / 1000.0; //Km //set the max flight line length double maxFLLengthKm = maxFLLengthMi * 5280.0 * 0.3048 / 1000.0; outfile.WriteLine(); outfile.WriteLine("Input flight line length is: " + maxFLLengthMi.ToString("F1") + " (mi) " + maxFLLengthKm.ToString("F1") + "( km)" ); //downrange image spacing including downlap //below used to compute the number of images per flight line double DRImageHeightWithDownlap = (1.0 - downlap / 100.0) * 2.0 * Math.Tan(DRFOV * Constants.Deg2Rad / 2.0) * flightAlt; //in feet //same as above but in km -- distance downrange between each image trigger double DRImageHeightKm = DRImageHeightWithDownlap * 0.3048 / 1000.0; //Km double aircraftSpeedKPH = aircraftSpeedKnots * 0.514 / 1000 * 3600.0; //0.514 m/sec per knot -- 100 knots = 51.4 m/s double forwardSpeedInClimb = 0.70 * aircraftSpeedKPH; //limit the forward speed when the aircraft is climbing to altitude double avgClimbRate = 500.0; //ft per minute -- climb rate double minimumFerryTime = (flightAlt / avgClimbRate) / 60.0; //time to climb to altitude double distanceDuringClimb = forwardSpeedInClimb * minimumFerryTime; //horizontal distance traveled during the climb to altitude //note -- this can make a flight line exceed the max length -- but not by too much. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// //at this point, we have the camera parameters //and all the coordinates of the polygon or linear path /////////////////////////////////////////////////////////////////////////////// //compute the LinearFeature coverage --- The below procedure will terminate the Planner if (p.polyCoverageType == COVERAGE_TYPE.linearFeature) { LinearFeaturePlanner(p, flightAlt, swathWidthKm, aircraftSpeedKnots, aircraftSpeedKPH, DRImageHeightKm); } ////////////////////////////////////////////////////////////////////////// //we will get to the below functionality only for the polygon planner ////////////////////////////////////////////////////////////////////////// //now determine if we prefere a NS or EW mission plan //compute the north-to-south distance wherein we will space the flightlines double NSDstanceKm = (p.maxNorthing - p.minNorthing) / 1000.0; double EWDstanceKm = (p.maxEasting - p.minEasting) / 1000.0; double NSDstanceMi = (p.maxNorthing - p.minNorthing) / 0.3048 / 5280.0; double EWDstanceMi = (p.maxEasting - p.minEasting) / 0.3048 / 5280.0; btnNS_EW.Visible = true; btnNS_EW.Enabled = true; int numSets = 0; int numFlightLines = 0; int maxSets = 20; int maxFlightLines = 200; double[, ,] XAllSets = new double[2, maxSets, maxFlightLines]; double[, ,] YAllSets = new double[2, maxSets, maxFlightLines]; /////////////////////////////////////////////////////////////////////////////////////////// //get the polygon coverage for three types of coverage and present the results to the user /////////////////////////////////////////////////////////////////////////////////////////// COVERAGE_SUMMARY covSummary1 = getPolygonFlightPathCoverage(0.0, //North-South case p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); COVERAGE_SUMMARY covSummary2 = getPolygonFlightPathCoverage(90.0 * Constants.Deg2Rad, //East-West case p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); COVERAGE_SUMMARY covSummary3 = //principal axis case getPolygonFlightPathCoverage((180.0-p.rotationAngleDeg) * Constants.Deg2Rad, p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); double flightTime1 = covSummary1.totalFlightLineLength / aircraftSpeedKPH + (covSummary1.numTotalFlightLines - 1) * timeToTurn; double flightTime2 = covSummary2.totalFlightLineLength / aircraftSpeedKPH + (covSummary2.numTotalFlightLines - 1) * timeToTurn; double flightTime3 = covSummary3.totalFlightLineLength / aircraftSpeedKPH + (covSummary3.numTotalFlightLines - 1) * timeToTurn; //display the results to the user and allow user to select from among the types of coverage Constants.Log(""); Constants.Log(" Coverage Options: "); Constants.Log(" Type #FL AvgFL (km) TotalLength (km) Time (hrs) "); String fmt1 = String.Format("NorthSouth \t{0,5} \t{1,8:##.00} \t{2,10:###.00} \t{3,6:#.00}", covSummary1.numTotalFlightLines, covSummary1.averageFlightLineLength, covSummary1.totalFlightLineLength, flightTime1); Constants.Log(fmt1); String fmt2 = String.Format("EastWest \t{0,5} \t{1,8:##.00} \t{2,10:###.00} \t{3,6:#.00}", covSummary2.numTotalFlightLines, covSummary2.averageFlightLineLength, covSummary2.totalFlightLineLength, flightTime2); Constants.Log(fmt2); String fmt3 = String.Format("PrincipalAxis \t{0,5} \t{1,8:##.00} \t{2,10:###.00} \t{3,6:#.00}", covSummary3.numTotalFlightLines, covSummary3.averageFlightLineLength, covSummary3.totalFlightLineLength, flightTime3); Constants.Log(fmt3); outfile.WriteLine(); outfile.WriteLine("Results of three types of coverage "); outfile.WriteLine(fmt1); outfile.WriteLine(fmt2); outfile.WriteLine(fmt3); //select the recommended coverage option from the three candidates if (flightTime1 < flightTime2 && flightTime1 < flightTime3) { btnNS_EW.Text = "North-South"; polyCoverageType = PolygonCoverageType.South2North; }; if (flightTime2 < flightTime1 && flightTime2 < flightTime3) { btnNS_EW.Text = "East-West"; polyCoverageType = PolygonCoverageType.East2West; }; if (flightTime3 < flightTime1 && flightTime3 < flightTime2) { btnNS_EW.Text = "PrincipleAxis"; polyCoverageType = PolygonCoverageType.PrincipalAxis; }; //enable the missionPlan startup button. button3.Visible = true; button3.Enabled = true; /////////////////////////////////////////////////////////////////////////////////// // wait here til user clicks considers the coverage options and clicks: "PLAN" /////////////////////////////////////////////////////////////////////////////////// while (!MissionPlanningInitiated) { Application.DoEvents(); //wait for user inputs } //redo the polygon coverage based on the user selection -- //need to save the old plans so we dont have to redo!! if ( polyCoverageType == PolygonCoverageType.South2North) getPolygonFlightPathCoverage(0.0, //North-South case p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); else if (polyCoverageType == PolygonCoverageType.East2West) getPolygonFlightPathCoverage(90.0 * Constants.Deg2Rad, //East-West case p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); else if (polyCoverageType == PolygonCoverageType.PrincipalAxis) getPolygonFlightPathCoverage((180.0-p.rotationAngleDeg) * Constants.Deg2Rad, p, swathWidthKm, maxFLLengthKm, minLineLength, DRImageHeightKm, ref numSets, ref numFlightLines, ref XAllSets, ref YAllSets); //this is placed here just in case the user has changed his mind and re-clicked a different airport p.selectedAirportIndex = selectedAirportIndex; GoogleMaps gglmaps = new GoogleMaps(datasetFolder, p.PolygonName); gglmaps.getGoogleMapToCoverProject(ref p, downloadMap); //insert the Google map from the web into this polygon ///////////////////////////////////////////////////////////////////////////////// bool replannerTool = false; //set true for a replanner tool ///////////////////////////////////////////////////////////////////////////////// //this is for a replanner tool // what does this do ????? //supposed to allow visual editing of the Quick-Look and replanning the mission to cover clouds & shadows List<endPoints> reflyLines = null; ReflyPlanner refly; if (replannerTool) { refly = new ReflyPlanner(@"E:\Melbourne\Melbourne_NEW_005\QLData\refly_005.kml", p); MessageBox.Show("refly tool is in effect"); //should have a way to indicate which photocenters are to be reflown reflyLines = refly.getReflySegments(); } ///////////////////////////////////////////////////////////////////////////////// //open the file to write the kml Project results kmlOutputFilename = datasetFolder + "\\" + p.PolygonName + ".kml"; //NOTE: this will fail if the input client polygon (.kml) is the same as the desired output kml FileStream fs2 = null; try { fs2 = new FileStream(kmlOutputFilename, FileMode.Create, FileAccess.Write, FileShare.None); } catch { MessageBox.Show("cant open the kml Project results file -- input file cannot be same as output file "); } kmlFlightLines = new StreamWriter(fs2); //kml file where we will write kmlFlightLines.AutoFlush = true; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //at this point, we have a 2D table YAllSets[numsets][numFlightLines] that describes the intended flightlines and lengths //however, some of these have zero length because of the irregular shape of the polygon //Moreover, we have not considered the max endurance -- so we must collect sequentual flightlines into flyable missions //Below we convert the 2D structure to a 1D structure containing only flightlines to be flown //we also assign the flightlines to individual missions ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// outfile.WriteLine(); outfile.WriteLine("Now have the set of flight lines by set"); outfile.WriteLine("Next break up these flight lines into time-constrained groupings to get a mission"); outfile.WriteLine(); Constants.Log("Accumulate flight lines into missions based on max mission length"); //go through the flightlines for a set and determine the total time-over-the-covered area for the complete Set //If this is > 2 hrs, then break the set into ~equal Area Of Interest (AOI) flight sessions "AOITimeMax". //find the start-end flightline for AOIs so the accumulated AOITime < AOITimeMax -- except the last AOI may be larger List<MissionSummary> MissionSumA = new List<MissionSummary>(); //set up for the polygon math polygonMath polyMath = new polygonMath(p.Easting, p.Northing, datasetFolder); double totalFlightlineDistance = 0; double flightlineDistanceThisMission = 0; int missionNumber = 0; int totalImages = 0; int totalImagesThisMission = 0; int startFlightlineThisMission = 0; double sumMissionLat = 0.0; //used to compute the average mission center (average lat/lon) double sumMissionLon = 0.0; double totalFerryTime = 0.0; double totalTimeToTurn = 0.0; double totalTimeOverTarget = 0.0; double latStart = 0, lonStart = 0; double latEnd = 0, lonEnd = 0; int jApt = 0; //airport counter double ferryTime = 0.0; double oldFerryTime = 0.0; int numberFlightlines = 0; outfile.Flush(); double avgMissionLat=0.0; double avgMissionLon=0.0; int numFL = 0; //numFL becomes the sequential flight line counter for the 1D array that will persist across the project for (int i = 0; i < numSets; i++) { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //compute the max allowable AOITime for this Set double maxAOITime = maxMissionTimeHrs; //maxAOITimeGlobal; outfile.WriteLine("Input Maximum Mission time is set to " + maxMissionTimeHrs.ToString()); double totalFlightlineDistancePerSet = 0; int numFLthisSet=0; int lastNonZeroFLThisSet = 0; //precompute the total time to cover the flight lines in this set for (int j = 0; j < numFlightLines; j++) { double FLLength = 0; double delEasting = XAllSets[1, i, j] - XAllSets[0, i, j]; double delNorthing = YAllSets[1, i, j] - YAllSets[0, i, j]; FLLength = Math.Sqrt(delEasting*delEasting + delNorthing * delNorthing); if (FLLength > 0) { numFLthisSet++; totalFlightlineDistancePerSet += FLLength; lastNonZeroFLThisSet = j; } } //total time for this set double totalAOITime = (totalFlightlineDistancePerSet / 1000.0) / aircraftSpeedKPH + (numFLthisSet - 1) * timeToTurn; double numberOfAOIsThisSet = Math.Floor(totalAOITime / maxAOITime) + 1.0; //adjust the maxAOITime so that it is <= to the input maxAOITime but equal for all missions within this set maxAOITime = totalAOITime / numberOfAOIsThisSet; outfile.WriteLine(); outfile.WriteLine("Number of missions this set = " + numberOfAOIsThisSet.ToString()); outfile.WriteLine("Adjusted Maximum Mission time to enforce equal-size missions this set " + maxAOITime.ToString("F1")); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //max allowable is now <= the initial max allowable -- reduced to have neary equal sized AOIs within the set. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int AOIsThisSet = 0; //////////////////////////////////////////////////////////////////////////////////// //now go back through the flight lines and break into groupings of flight lines //this will define the flyable missions with a prescribed time-over-target threshold //////////////////////////////////////////////////////////////////////////////////// for (int j = 0; j < numFlightLines; j++) { //compute the flight line length using the north and south intersection extents //YAllSets[1, i, j] is the flight line "end" -- the larger Northing value (i.e., northmost end) double flightLineDistanceKm = 0; double delEasting = XAllSets[1, i, j] - XAllSets[0, i, j]; double delNorthing = YAllSets[1, i, j] - YAllSets[0, i, j]; flightLineDistanceKm = Math.Sqrt(delEasting * delEasting + delNorthing * delNorthing) / 1000.0; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (flightLineDistanceKm == 0 ) continue; //skip the below computations id the terminated flight line has zero length //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //number of flightlines this mission is current flight line (numFL) minus start flight line numberFlightlines = numFL - startFlightlineThisMission + 1; //num of flight lines this mission //sum the total flight line distances //images this flight line based on the downrange image extent int imagesThisFlightline = Convert.ToInt32(Math.Floor(flightLineDistanceKm / DRImageHeightKm) + 1.0); //per-mission and total travel distances and num of images totalFlightlineDistance += flightLineDistanceKm; flightlineDistanceThisMission += flightLineDistanceKm; totalImages += imagesThisFlightline; totalImagesThisMission += imagesThisFlightline; //transfer the start/end UTM coordinates back to the LatLon for use in the kml file //Y is northing and X is easting utm.UTMtoLL(YAllSets[0, i, j], XAllSets[0, i, j], p.UTMZone, ref latStart, ref lonStart); utm.UTMtoLL(YAllSets[1, i, j], XAllSets[1, i, j], p.UTMZone, ref latEnd, ref lonEnd); FLSum[numFL].planned.startUTMX = XAllSets[0, i, j]; FLSum[numFL].planned.startUTMY = YAllSets[0, i, j]; FLSum[numFL].planned.endUTMX = XAllSets[1, i, j]; FLSum[numFL].planned.endUTMY = YAllSets[1, i, j]; //fill the flightline summary structure FLSum[numFL].planned.startLat = latStart; FLSum[numFL].planned.startLon = lonStart; FLSum[numFL].planned.endLat = latEnd; FLSum[numFL].planned.endLon = lonEnd; FLSum[numFL].numImages = imagesThisFlightline; FLSum[numFL].lineNumberInPolygon = numFL; FLSum[numFL].flightlineLengthMi = flightLineDistanceKm * 1000.0 / 0.3048 / 5280.0; FLSum[numFL].FLstat = fightlineStatus.plan; //sums to be used to compute the average mission center sumMissionLat += (FLSum[numFL].planned.startLat + FLSum[numFL].planned.endLat) / 2.0; sumMissionLon += (FLSum[numFL].planned.startLon + FLSum[numFL].planned.endLon) / 2.0; ///////////////////////////////Closest Airport/////////////////////////////////////////////////////////////////// //average mission lat/lon for use in getting the ferry distance //we keep a running sum to get the avg as we add flight lines to the mission avgMissionLat = sumMissionLat / numberFlightlines; avgMissionLon = sumMissionLon / numberFlightlines; /* //closest airport and ferry time to average mission location //test all candidate airports and use the closest to the mission center double minAptDistance = 99999999999999.0; for (int jA = 0; jA < p.airports.Count; jA++) { double delLat = p.airports[jA].lat - avgMissionLat; double delLon = p.airports[jA].lon - avgMissionLon; double dist = Math.Sqrt(delLat * delLat + delLon * delLon); if (dist < minAptDistance) { minAptDistance = dist; jApt = jA; } //store the index of the closest airport } */ double aptNorthing = 0.0, aptEasting = 000, avgNorthing = 0.0, avgEasting = 0.0; //convert the airport lat/Long (LL) coordinates to UTM for distance measurement utm.LLtoUTM(p.airports[p.selectedAirportIndex].lat * Constants.Deg2Rad, p.airports[p.selectedAirportIndex].lon * Constants.Deg2Rad, ref aptNorthing, ref aptEasting, ref p.UTMZone, true); utm.LLtoUTM(avgMissionLat * Constants.Deg2Rad, avgMissionLon * Constants.Deg2Rad, ref avgNorthing, ref avgEasting, ref p.UTMZone, true); //delta N and E distances in Miles double dn = (aptNorthing - avgNorthing) / 1000.0; double de = (aptEasting - avgEasting) / 1000.0; //ferry distance one-way -- approximation cause the ferry time really to the start of the first line of this mission double ferryDistanceKm = Math.Sqrt(dn * dn + de * de); oldFerryTime = ferryTime; // save the last ferry time --- new ferry time assuming we keep the current mission is below if (ferryDistanceKm > distanceDuringClimb) //account for the travel portion while climbing to altitude ferryTime = minimumFerryTime + (ferryDistanceKm - distanceDuringClimb) / aircraftSpeedKPH + ferryDistanceKm / aircraftSpeedKPH + takeoffTime + landingTime; else //closest airport is inside the mission area -- climb time is purely addidive to the flight time ferryTime = minimumFerryTime + ferryDistanceKm / aircraftSpeedKPH + takeoffTime + landingTime; /////////////////////////////////Closest AIRPORT//////////////////////////////////////////////////////////////// numFL++; //number of flight lines that have been investigated ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //separate the flight lines into multiple missions by asking: If we add this flight line do we exceed the aircraft endurance? //also do this for the last flightline in each Set -- and the very last flight line. //note -- on the first entry, we havent computed a ferry time (its zero) but its computed below double expectedAOITime = (flightlineDistanceThisMission) / aircraftSpeedKPH + (numberFlightlines - 1) * timeToTurn; outfile.WriteLine(String.Format(" New flightline: {0,2} length {1,10:#.00} (mi) accumulated Time = {2,10:#.00}", numFL, flightLineDistanceKm * 1000.0 / 0.3048 / 5280.0, expectedAOITime)); outfile.Flush(); //NOTE: expectedAOITime includes the current line being tested //will we allow each AOI to be slightly larger than maxAOITime (by one flight line) or force each AOI to be smaller by one flight line? //for the first case, we will end up with a short AOI at the end -- for the second case, we end up with a long AOI at the end. //at the very end, we always want to set the prior lines in this Set as an AOI //first part of the "if" test tests to see if adding this flight line exceeds the maxAOITime. //note that this test is removed if we are at the last AOI within a set. This allows the last AOI to be established //even though the AOITime is below the maxAOITime. The second part of the "if" test triggers the AOI at the very last nonzero flightline. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if ((expectedAOITime > maxAOITime && AOIsThisSet != (numberOfAOIsThisSet-1) ) || (j == lastNonZeroFLThisSet)) { AOIsThisSet++; //numFL is the total number of flight lines that have been processed below //numFL-1 is the flightline ID of the last fight line that was processed //numFL will be the next flight line process so is the start line of the next mission //below logic accounts for the potential staggered start and ends of flight lines //these computations assume you start at the west headed north and turn back south after each north-going line, alternating north/south directions double extendedTravelToNextLine = 0; // accounts for the distance to travel to the start of the next flight line for (int k = startFlightlineThisMission; k < (numFL-2) ; k += 2) { //account for extended line going north and turning south (end is at the north) extendedTravelToNextLine += Math.Abs(FLSum[k].planned.endUTMX - FLSum[k + 1].planned.endUTMX) / 1000.0; //account for extended line going south and turning north (start is at the south) extendedTravelToNextLine += Math.Abs(FLSum[k + 1].planned.startUTMX - FLSum[k + 2].planned.startUTMX) / 1000.0; } flightlineDistanceThisMission += extendedTravelToNextLine; totalTimeToTurn += (numberFlightlines - 1) * timeToTurn; //Store the mission summary data into a structure and add to the ArrayList Collection MissionSummary missionSum = new MissionSummary(); missionSum.missionPolygon = new List<PointD>(); // this is a geodetic polygon formed from the flight line endpoints missionSum.missionNumber = missionNumber; //incremental index of the missions missionSum.startFlightLine = startFlightlineThisMission; //starting flight line for this mission missionSum.FerryTime = ferryTime; //two-way ferry --- computed below missionSum.LengthMi = flightlineDistanceThisMission * 1000.0 / 0.3048 / 5280.0; missionSum.numberFlightlines = numberFlightlines; // missionSum.TimeOverTarget = flightlineDistanceThisMission / aircraftSpeedKPH + (numberFlightlines - 1) * timeToTurn; totalTimeOverTarget += missionSum.TimeOverTarget; missionSum.MissionStat = MissionStatus.plan; missionSum.takeoffAirport = p.airports[jApt]; missionSum.totalImages = totalImagesThisMission; missionSum.backgroundImageNWCorner = new PointD(0.0, 0.0); missionSum.backgroundImageSECorner = new PointD(0.0, 0.0); outfile.WriteLine(String.Format(" new misson: {0,2} startFlightLine {1,3} numFlightLines {2,3} flightlineDistance {3,10:#.00} (Mi) ferryTime {4,5:#.00} (hr) ", missionSum.missionNumber, missionSum.startFlightLine, missionSum.numberFlightlines, missionSum.LengthMi, missionSum.FerryTime)); missionSum.backgroundImageFilename = "NA"; missionSum.setNumber = i; if (!replannerTool) { //form a polygon of the mission envelope based upon the flight line endpoints. //I dont see where these UTM endpoints are used!!! List<double> EastingMsn = new List<double>(); List<double> NorthingMsn = new List<double>(); //go around the flight line endpoints beginning at the south ends and moving counterclockwise for (int ifl = startFlightlineThisMission; ifl < (startFlightlineThisMission + numberFlightlines); ifl++) { EastingMsn.Add(FLSum[ifl].planned.startUTMX); NorthingMsn.Add(FLSum[ifl].planned.startUTMY); missionSum.missionPolygon.Add(new PointD(FLSum[ifl].planned.startLon, FLSum[ifl].planned.startLat)); } //add the north ends starting at the westmost lines so the poly forms a sequential set of points counterclockwise for (int ifl = (startFlightlineThisMission + numberFlightlines - 1); ifl > startFlightlineThisMission - 1; ifl--) { EastingMsn.Add(FLSum[ifl].planned.endUTMX); NorthingMsn.Add(FLSum[ifl].planned.endUTMY); missionSum.missionPolygon.Add(new PointD(FLSum[ifl].planned.endLon, FLSum[ifl].planned.endLat)); } //add one more point back at the start to create a linesegment to close the polygon missionSum.missionPolygon.Add(new PointD(FLSum[startFlightlineThisMission].planned.startLon, FLSum[startFlightlineThisMission].planned.startLat)); } else { //form the mission polygon from the flight line bounding boxes double maxLat = -99999.0, maxLon = -99999.0; double minLat = 99999.0, minLon = 99999.0; foreach (endPoints ep in reflyLines) { if (ep.startLat > maxLat) maxLat = ep.startLat; if (ep.endLat > maxLat) maxLat = ep.endLat; if (ep.startLon > maxLon) maxLon = ep.startLon; if (ep.endLon > maxLon) maxLon = ep.endLon; if (ep.startLat < minLat) minLat = ep.startLat; if (ep.endLat < minLat) minLat = ep.endLat; if (ep.startLon < minLon) minLon = ep.startLon; if (ep.endLon < minLon) minLon = ep.endLon; } missionSum.missionPolygon.Add(new PointD(minLon-0.01, maxLat+0.01)); missionSum.missionPolygon.Add(new PointD(maxLon+0.01, maxLat+0.01)); missionSum.missionPolygon.Add(new PointD(maxLon+0.01, minLat-0.01)); missionSum.missionPolygon.Add(new PointD(minLon-0.01, minLat-0.01)); missionSum.missionPolygon.Add(new PointD(minLon-0.01, maxLat+0.01)); } //mission center/average lat/lon is computed below this statement missionSum.missionCenter = new PointD(avgMissionLat, avgMissionLon); outfile.Flush(); //////////// add mission summary to the ArrayList collection of missions/////////////////////////////// MissionSumA.Add(missionSum); totalFerryTime += ferryTime; //total ferry times across the complete project -- used for the average ferry per mission flightlineDistanceThisMission = 0; //reset the mission distance summation totalImagesThisMission = 0; //reset the total images this mission missionNumber++; //increment the mission couinter startFlightlineThisMission = numFL; //set the start flight line for the next mission sumMissionLat = 0.0; //reset the sums used for the current mission lat/lon average sumMissionLon = 0.0; } } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //cycle back through the missions and get the maps and the average altitude //do this in a batch rather than in the per-mission segment above //in order to better batch-submit requests to the Google Maps API ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// List<PointD> totalPointsInPoly = new List<PointD>(); //a list of Npts points randomly spaced withi each polygon; int Npts = 100; //number of random-spaced points from each mission polygon used to form the average elevation in the mission polygon // MissionSumA is the collection of all missions -- this includes the mission polygon that is determined above for (int i = 0; i < MissionSumA.Count; i++) { //////////////////////////////////////////////////////////// // not quite sure why we do the intermediate storage !!! //////////////////////////////////////////////////////////// MissionSummary missionSum = MissionSumA[i]; //get the map of the mission area gglmaps.createBackgroundMaps(ref missionSum, FLSum, p.UTMZone, downloadMap); MissionSumA[i] = missionSum; //get a list of all the accumulated Npts points used for all the averages //this is done so we can request the elevation using Google Maps API with a minimum of requests // getPointsInPoly returns a list of Npts PointD points -- so the below call adds Npts on each call ??? for (int ips = 0; ips < Npts; ips++) { totalPointsInPoly.Add(polyMath.getPointsInPoly(Npts, missionSum.missionPolygon)[ips]); } } //this procedure gets the elevations for the totalPointsInPoly using the Google Elevation API //the points are requested using the polyline request method to minimize the number of requests. //we pack as many elevation points into each request as possible within the constraints of the URL length //The elevation API limits requests to 2500/day and a total of 25000 elevation points per day. //so 2500 requests with 100 points per request will hit the limit. List<double> heights = polyMath.getTerrainHeightsFromPointList(totalPointsInPoly); //go back through the total list of elevations and break the list into missions and get the elevation stats per mission for (int i = 0; i < MissionSumA.Count; i++) { MissionSummary missionSum = MissionSumA[i]; List<double> heightsThisMission = new List<double>(); for (int ips = i * Npts; ips < (i + 1) * Npts; ips++) heightsThisMission.Add(heights[ips]); heightsThisMission.Sort(); //used to get the 90 percentile height missionSum.ts.avgTerrainHeight = heightsThisMission.Average(); missionSum.ts.terrain90percentileHeight = heightsThisMission[Convert.ToInt32(0.90 * Npts)]; missionSum.ts.maxTerrainHeight = heightsThisMission.Max(); missionSum.ts.minTerrainHeight = heightsThisMission.Min(); ////flight altitude in feet for the pilot display missionSum.FlightAltMSLft = flightAlt + missionSum.ts.terrain90percentileHeight / 0.3048; //flightAlt is AGL in ft; ////round to the nearest 100 ft missionSum.FlightAltMSLft = (int)(missionSum.FlightAltMSLft / 100.0 + 0.50) * 100.00; MissionSumA[i] = missionSum; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //now we have all the data for the missions. //we need to write out the xml text lines that will be inserted into the original kml file. //we insert beneath the original Placemark element (after the <Placemark> and form a <Folder> kml structure //each element in the folders will contain one mission //also each mission folder will contain multiple Placemark elements that represent the flightlines //note the <description> <!CDATA[ ..... ]]> .... this is inserted HTML code to format the presentation ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Constants.Log(" Write the mission plan .kml"); //////////////////////////////// //write the kml file //////////////////////////////// writeKmlPolygonMissonPlan(p, DRImageHeightKm, missionNumber, flightAlt, swathWidthKm, totalFlightlineDistance, totalFerryTime, aircraftSpeedKPH, totalTimeOverTarget, numFL, totalImages, MissionSumA); /////////////////////////////////////////////////////////////// //the above comprises a complete closed set of .kml elements /////////////////////////////////////////////////////////////// kmlFlightLines.Flush(); Constants.Log(""); Constants.Log(""); Constants.Log("Normal Termination "); Constants.Log(""); ////////////////////////////////////////////// //show the END button button2.Visible = true; button2.Enabled = true; ////////////////////////////////////////////// } //Application.Exit(); }