Wednesday, November 30, 2011

Simple Steps for GPS use and data capture.

If you are establishing a database where you collect GPS points, for reporting, asset management or any other new project, here are some simple tips. This posting is ideally for people working in an NGO, community group, personal GPS Project, or in places without a well known or established geographic infrastructure such as Iraq and Afghanistan, or any place where you are not sure what local datum to use.


How GPS Works

  • Fixed orbiting Satellites, with super accurate clocks, send a repeating weak data stream containing satellite ID signals and a time stamp.
  • Earth based devices use the subtle differences from each signal to work out where the satellites are in relation to an imaginary sphere that aligns closely with the earth.
  • Trees, buildings and cars bounce signals, clouds and the atmosphere delay the signals, and other effects cause your GPS calculated to "drift" around its trigonometry calculated location.
  Basic Settings for a GPS

1 Set all of the GPS devices to Decimal Degrees - DD.DDDDD using WGS84 as the datum. This works world wide, even in Antarctica.
2 Capture and store all your data in a spreadsheet as Decimal Degrees to 5 or 6 decimal places depending on how accurate you need your data to be. You can do this manually if you have only a few places, via a note paper in the field, or you can capture "Waypoints" or bookmarks on the device and translate them later, or extract via bluetooth or cable.
Five DP allows you to capture data to 1.1m resolution which is ideal for most hand held GPS, which in normal conditions will provide 3-5m accuracy at most. 6 decimal places will give your data a resolution of 11cm depending on the GPS Unit quality - 7 DP is only useful if you have a high performance GPS device - otherwise its 2 more digits that you will use and possibly introduces false confidence and mistakes through human error. Resolution should not be confused with Accuracy.

3 Store all of you GPS features/asset data as 2 fields in your databases, numeric, 00.00000 and 00.00000.
Example:
5 Decimal Places such as these numbers puts you on the roundabout outside Safi Landmark Hotel in Kabul Afghanistan.
   34.53300, 69.16539

This example puts you on a roundabout in the former Green Zone Iraq.
   33.30221, 44.39885

This example with only 4 Decimal places puts you in a Pool at Saddams former palace where I had many swims.
   33.3030, 44.4085

A Truck on a Road in Central Sudan.
   14.243032, 32.988980
A Mosque in Khartoum.
   15.48064, 32.50142
4 If you want to validate the data collected, use bounding fields covering the country such as must be more than 25 and less than 40 for N, and E must be greater than 50 and less than 80 or something like this (Afghanistan). For a small study area you can be more restructive. Compare the values above and you can see the difference between Sudan DD.DDDDD and Afghanistan DD.DDDDD references. If your sponsor requires other formats, then provide those using formula in Excel or batch online processing, there a numerous free online sites that will provide this as a free batch service. Do not store these translated values in your database. MGRS for example - as a civilian organisation you have no business using this data type. Do not store your data using this format.


Degrees, Minutes and Seconds Degrees, Minutes and Seconds provide almost no value to anyone except geeks in which case ask them to explain why they are causing such pain. They are difficult for normal people to use, difficult to find on a map, and being based on Imperial 60 base, most people can’t understand the divide by/multiply by 60 translation formulas. GIS and Map programs have difficulty reading this format as it needs to be stored as a text value, also making data quality difficult to monitor and you cant apply the range bounding box QA checker described above. Don’t put both data into the same field, use two as per below.


What do you do with the Data
To capture the data you will want to store information in a series of table headings in an Excel which can be used by any GIS technician to load into a database or map. Standardise your fields where possible, eg:

NAME DD.DDDDD DD.DDDDD DATE BY
Mosque 15.48064 32.50142 04JUL11 Eng. Suliman
You might also like to have a field for “CapturedBy” as a data source, link to a photo name, and any other information about the subject feature you want to capture, such as description, colour, type of Item etc. Keep the formatting to a minimum, and don’t store it in a word document as you will end up putting it back into excel. Use only Excel. Keep photographs in a separate hyper linkable folder with a naming convention, such as a unique ID that can be linked back to from excel.


Then You can Make a Map
Any GIS software should be able to import from an Excel sheet, or a saved as CSV file and use the coordinates to plot the locations as sites on a map, using any other fields as labels and symbology qualifiers. This short guide was originally written late one night for a colleague based near Kandahar for very basic starter data capture in a difficult environment.

Thursday, September 15, 2011

Change the Date Format of the AttributeInspector (EditWidget.mxml)


/*
   This example provides a workaround for changing the date format of the AttributeInspector
   targeting the ArcGIS Viewer for Flex 2.4.
   Remarks: This workaround was developed to provide a way of changing the date format     
   of AttributeInspector for fields of type CalendarField. This type is associated with
   FormItems of DateFields of the AttributeInspector and isn't available for developers.

   Case Esri modifies or implements this feature in a better way than that implementation
   should be used.
                       
   The function msToDate is part of the ArcGIS Viewer for Flex 2.4. This method should be
   placed as a Util file to be shared across different classes.

   Author: José Sousa
   Date: 15-09-2011
*/


*/
   Locate the populateEditor private function and place inside of the statement
   if(featureLayers.length > 0) just below the declaration
   editor.attributeInspector.infoWindowLabel = attributesLabel; the code below  
   editor.attributeInspector.addEventListener(AttributeInspectorEvent.SHOW_FEATURE, 
   attributeInspector_showFeatureHandler);
   editor.attributeInspector.addEventListener(AttributeInspectorEvent.UPDATE_FEATURE,
   attributeInspector_updateFeatureHandler);
/*

 
// Place the following code below the function populateEditor
private var dateFormatter:DateFormatter = new DateFormatter();
private const newDateFormat:String = "DD/MM/YYYY";

private function attributeInspector_showFeatureHandler(event:AttributeInspectorEvent):void
{
   formatDateFieldDisplay(event);
}
private function attributeInspector_updateFeatureHandler(event:AttributeInspectorEvent):void
{
   formatDateFieldDisplay(event);
}
private function formatDateFieldDisplay(event:AttributeInspectorEvent):void
{
   if(!(event.featureLayer && event.feature && event.feature.attributes))
      return;
   // This ensures that we only validate when we are working at a field level
   (not an AttributeInspector level)
   if(event.field)
   {
      if(event.field.type == Field.TYPE_DATE)
      {
         if(event.newValue) // If it comes exclusively from an update ... format the display
         {
            // Change the date format as desired
            event.target.formatString = newDateFormat;
         }
      }
   }
   else if(event.type.toLowerCase() == "showfeature")
   {
      var formItems:* = editor.attributeInspector.form.getChildren();
     
      // Loop items and trigger update event
      for (var i:Number = 0; i < formItems.length; i++)
      {
         if(formItems[i].data && formItems[i].data is Field)
         {
            var field:Field = formItems[i].data;
            if(field.type == Field.TYPE_DATE &&
               field.name in event.feature.attributes &&    
               event.feature.attributes[field.name])
            {
               var event:AttributeInspectorEvent = new AttributeInspectorEvent("updateFeature",
                                                                 true,
                                                                 event.featureLayer,
                                                                 event.feature,
                                                                 field,
                                                                 event.feature.attributes[field.name],
                                                                 event.feature.attributes[field.name]);
               var formItem:* = formItems[i].getChildren();
               if(formItem && formItem.length > 0)
                  formItem[0].dispatchEvent(event);
            }
         }
      }
   }
}

// This function should be in a separate util file to be reused if necessary by other classes.
// This function comes with the ArcGIS Viewer for Flex 2.4.
// If Esri updates this function so should the developer.
private function msToDate(ms:Number, dateFormat:String, useUTC:Boolean):String
{
   var date:Date = new Date(ms);
   if (date.milliseconds == 999) // Workaround for REST bug
      date.milliseconds++;

   if (useUTC)
      date.minutes += date.timezoneOffset;
   if (dateFormat)
   {
      dateFormatter.formatString = dateFormat;
      var result:String = dateFormatter.format(date);
     
      if (result)
         return result;
      else
         return dateFormatter.error;
   }
   else
   {
      return date.toLocaleString();
   }
}

Monday, March 21, 2011

Determine the Midpoint for a Polyline for ArcGIS API for Flex

/*
This method determines the midpoint of the specified polyline using the ArcGIS API for Flex geometry types. It is required that the coordinate system used by the geometry is Cartesian.

Remarks: This method does not support multipart objects. If multipart is necessary, it is required                some adjustments to the core functionality.
Author: José Sousa
Date: 22-03-2011
*/
private function GetPolylineMidpoint(polyline:Polyline):MapPoint
{  
   if(polyline == null || polyline.paths == null || polyline.paths.length < 1)
      return null;
                       
   // Does not support multiparts (in this case the center could be outside of the polylines itself)
   var totalDistance:Number = getPolylinePathLength(polyline.paths[0]); // Just get's the first path
   if(totalDistance <= 0)
      return null;
                     
   // Get the midpoint
   return getMidPoint(polyline.paths[0], totalDistance);
}
                 
private function getPolylinePathLength(points:Array):Number
{  
   var distance:Number = 0.0;                     
   for(var i:Number = 0; i < points.length - 1; i++)  
   {      
      distance += getDistance(points[i], points[i + 1]);  
   }                      
   return distance;
}
              
private function getMidPoint(points:Array, totalDistance:Number):MapPoint
{  
   var halfDistance:Number = totalDistance / 2;
   var sumDistance:Number = 0.0;  
   var distance:Number = 0.0;  
   var i:Number;
                     
   for(i = 0; i < points.length - 1; i++)  
   {      
      distance = getDistance(points[i], points[i + 1]);
      if((sumDistance + distance) < halfDistance)
      {
         sumDistance += distance;      
      }
      else
      {
         break
      }
   }
                      
   distance = halfDistance - sumDistance;                    
   return getCoordinate(points[i], points[i + 1], distance);
}
              
private function getCoordinate(fromPoint:MapPoint, toPoint:MapPoint, queryDistance:Number):MapPoint
{    
   var angle:Number;
   var distance:Number = getDistance(fromPoint, toPoint);
   var coordX:Number;
   var coordY:Number;                       
                       
   if((toPoint.x - fromPoint.x >= 0) && (toPoint.y - fromPoint.y >= 0))
   {
      angle = Math.asin((toPoint.y - fromPoint.y) / distance);                           
      coordX = queryDistance * Math.cos(angle) + fromPoint.x;
      coordY = queryDistance * Math.sin(angle) + fromPoint.y;
   }  
   else if((toPoint.x - fromPoint.x < 0) && (toPoint.y - fromPoint.y >= 0))
   {
      angle = Math.asin((fromPoint.x - toPoint.x) / distance);                                                               
      coordX = fromPoint.x - queryDistance * Math.sin(angle);
      coordY = queryDistance * Math.cos(angle) + fromPoint.y;
   }  
   else if((toPoint.x - fromPoint.x <= 0) && (toPoint.y - fromPoint.y < 0))
   {
      angle = Math.asin((fromPoint.y - toPoint.y) / distance);                                 
      coordX = fromPoint.x - queryDistance * Math.cos(angle);
      coordY = fromPoint.y - queryDistance * Math.sin(angle);
   } 
   else if((toPoint.x - fromPoint.x > 0) && (toPoint.y - fromPoint.y < 0))
   {
      angle = Math.asin((toPoint.x - fromPoint.x) / distance);                                                               
      coordX = queryDistance * Math.sin(angle) + fromPoint.x;
      coordY = fromPoint.y - queryDistance * Math.cos(angle);
   }
                                                                   
   if(fromPoint.spatialReference)
      return new MapPoint(coordX, 
                          coordY,
                          new SpatialReference(
                              fromPoint.spatialReference.wkid,   
                              fromPoint.spatialReference.wkt));
   return new MapPoint(coordX, coordY, null);
}

private function getDistance(point1:MapPoint, point2:MapPoint):Number
{
   return Math.sqrt((point2.x - point1.x) * (point2.x - point1.x) + 
                    (point2.y - point1.y) * (point2.y - point1.y));                  
}