I was tasked to create a curved polyline using Flex API and wanted to share my work.

4399
3
11-08-2011 12:33 PM
RahulMetangale
New Contributor
ArcGIS API for Flex does not support curved geometries directly, so instead we must approximate the shape of a geodesic curve by creating a polyline containing several small segments. Using a larger number of segments will make the polyline appear more smooth and more closely resemble the shape of the smooth curve, but will also increase its complexity. Using 32 segments is more than sufficient accuracy for most maps. We???ll call this value n.

var n = 32; 


Then, we need to determine the overall extent of the route, which we???ll call d. The shortest distance between any two points on a sphere is the great circle distance. Assuming that the coordinates of the start and end points are (lat1, lon1) and (lat2, lon2) respectively, measured in Radians, then we can work out the great circle distance between them using the Haversine formula, as follows:

var d = 2 * asin(sqrt(pow((sin((lat1 - lat2) / 2)), 2) + cos(lat1) * cos(lat2) * pow((sin((lon1 - lon2) / 2)), 2)));


We then determine the coordinates of the endpoints of each segment along the geodesic path. If f is a value from 0 to 1, which represents the percentage of the route travelled from the start point (lat1,lon1) to the end point (lat2,lon2), then the latitude and longitude coordinates of the point that lies at f proportion of the route can be calculated as follows:

var A = sin((1 - f) * d) / sin(d);
var B = sin(f * d) / sin(d);

// Calculate 3D Cartesian coordinates of the point
var x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
var y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
var z = A * sin(lat1) + B * sin(lat2);

// Convert these to latitude/longitude
var lat = atan2(z, sqrt(pow(x, 2) + pow(y, 2)));
var lon = atan2(y, x);


By repeating the above with different values of f, (the number of repetitions set according to the number of segments in the line), we can construct an array of latitude and longitude coordinates at set intervals along the geodesic curve from which a polyline can be constructed.

Complete function:
private function ToGeodesic(points:Array, n:int):Array {
    if (!n) { n = 32 }; // The number of line segments to use
    var locs:Array = new Array();
    for (var i:int = 0; i < points.length - 1; i++) {
     with (Math) {
      // Convert coordinates from degrees to Radians
      var lat1:Number = points.y * (PI / 180);
       var lon1:Number = points.x * (PI / 180);
       var lat2:Number = points[i + 1].y * (PI / 180);
       var lon2:Number = points[i + 1].x * (PI / 180);
      // Calculate the total extent of the route
       var d:Number = 2 * asin(sqrt(pow((sin((lat1 - lat2) / 2)), 2) + cos(lat1) * cos(lat2) * pow((sin((lon1 - lon2) / 2)), 2)));
      // Calculate  positions at fixed intervals along the route
      for (var k:int = 0; k <= n; k++) {
        var f:Number = (k / n);
        var A:Number = sin((1 - f) * d) / sin(d);
        var B:Number = sin(f * d) / sin(d);
       // Obtain 3D Cartesian coordinates of each point
        var x:Number = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
       var y:Number = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
        var z:Number = A * sin(lat1) + B * sin(lat2);
       // Convert these to latitude/longitude
        var lat:Number = atan2(z, sqrt(pow(x, 2) + pow(y, 2)));
        var lon:Number= atan2(y, x);
       // Create a Location (remember to convert back to degrees)
        var p:MapPoint = new MapPoint(lon / (PI / 180),lat / (PI / 180));
       // Add this to the array
       locs.push(p);
      }
     }
    }
    return locs;
    
   }



Sample Application:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
      xmlns:s="library://ns.adobe.com/flex/spark"
      xmlns:esri="http://www.esri.com/2008/ags"
      pageTitle="Geodetic Curve" 
      applicationComplete="application1_applicationCompleteHandler(event)">
 <!--
 This sample shows how to create geodetic line.
 ArcGIS Flex API does not support curved geometries directly, so instead we must approximate the shape of a geodesic curve 
 by creating a polyline containing several small segments. 
 Using a larger number of segments will make the polyline appear more smooth and more closely resemble the shape of the smooth 
 curve, but will also increase its complexity. 
 I find that using 32 segments is more than sufficient accuracy for most maps. 
 
 -->
 <fx:Script>
  <![CDATA[
   import com.esri.ags.Graphic;
   import com.esri.ags.SpatialReference;
   import com.esri.ags.geometry.MapPoint;
   import com.esri.ags.geometry.Multipoint;
   import com.esri.ags.geometry.Polyline;
   import com.esri.ags.symbols.SimpleLineSymbol;
   
   import mx.events.FlexEvent;
   
   private function addLine():void
   {
    //create a straight line between two points
   var myPolyline:Polyline = new Polyline(
    [[
     new MapPoint(19.04,51.16),
     new MapPoint(-93.98,45.44)
     
    ]], new SpatialReference(4326));
   var myGraphicLine:Graphic = new Graphic(myPolyline);
   myGraphicLine.symbol = new SimpleLineSymbol(SimpleLineSymbol.STYLE_DASH, 0xDD2222, 1.0, 4);
   myGraphicsLayer.add(myGraphicLine);
   
   //Point Array
   var pointarray:Array=new Array(new MapPoint(19.04,51.16),new MapPoint(-93.98,45.44));
   
   //Create Geodetic line
   
   //Following function takes two input: PointArray and number of Segments and returns and Array
   var Geoarr:Array = ToGeodesic(pointarray,32);
   var geoPolyline:Polyline=new Polyline();
   geoPolyline.addPath(Geoarr);
   var GeoLine:Graphic = new Graphic(geoPolyline);
   GeoLine.symbol = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, 0xDD2222, 1.0, 4);
   myGraphicsLayer.add(GeoLine);
   
   }
   
   // Creates geodesic approximation of the lines drawn between an array
   // of points, by dividing each line into a number of segments.

  private function ToGeodesic(points:Array, n:int):Array {
    if (!n) { n = 32 }; // The number of line segments to use
    var locs:Array = new Array();
    for (var i:int = 0; i < points.length - 1; i++) {
     with (Math) {
      // Convert coordinates from degrees to Radians
      var lat1:Number = points.y * (PI / 180);
       var lon1:Number = points.x * (PI / 180);
       var lat2:Number = points[i + 1].y * (PI / 180);
       var lon2:Number = points[i + 1].x * (PI / 180);
      // Calculate the total extent of the route
       var d:Number = 2 * asin(sqrt(pow((sin((lat1 - lat2) / 2)), 2) + cos(lat1) * cos(lat2) * pow((sin((lon1 - lon2) / 2)), 2)));
      // Calculate  positions at fixed intervals along the route
      for (var k:int = 0; k <= n; k++) {
        var f:Number = (k / n);
        var A:Number = sin((1 - f) * d) / sin(d);
        var B:Number = sin(f * d) / sin(d);
       // Obtain 3D Cartesian coordinates of each point
        var x:Number = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
       var y:Number = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
        var z:Number = A * sin(lat1) + B * sin(lat2);
       // Convert these to latitude/longitude
        var lat:Number = atan2(z, sqrt(pow(x, 2) + pow(y, 2)));
        var lon:Number= atan2(y, x);
       // Create a Location (remember to convert back to degrees)
        var p:MapPoint = new MapPoint(lon / (PI / 180),lat / (PI / 180));
       // Add this to the array
       locs.push(p);
      }
     }
    }
    return locs;
    
   }


   protected function application1_applicationCompleteHandler(event:FlexEvent):void
   {
    // TODO Auto-generated method stub
    addLine();
   }

  ]]>
 </fx:Script>
 <esri:Map >
  <esri:ArcGISTiledMapServiceLayer url="http://server.arcgisonline.com/ArcGIS/rest/services/NGS_Topo_US_2D/MapServer"/>
  <esri:GraphicsLayer id="myGraphicsLayer"/>
 </esri:Map>
 
</s:Application>
Tags (2)
0 Kudos
3 Replies
by Anonymous User
Not applicable
Thanks Rahul for sharing.  This is good stuff.
0 Kudos
JamesOutlaw
New Contributor II
Thanks a lot. I was just about to tackle this on my own and you have helped me save a lot of time!
0 Kudos
GregoryKramida
New Contributor
Very nice, Rahul.

I can also sugguest to developers to extend the LineSymbol class itself and override the draw function:
override public function draw(sprite:Sprite, geometry:Geometry, attributes:Object, map:Map):void{...}


In here, where you get your POLYLINE, you can examine the geometry.paths and, for each path, convert to screen-point array (you can do the same for POLYGON rings):
var path:Array, screenPath:Array;
var screenPaths:Array = [];
for each (path in line.paths) {
 screenPath = [];
 for (var i:int = 0; i < path.length; i++){
  screenPath.push(new Point(toScreenX(map,path.x),toScreenY(map,path.y)));
 }
 screenPaths.push(screenPath);
}


Now you should have an array of arrays of points that have screen coordinates. You can do clipping however you like on those, but now you can render them any way you want. One way is to use cubic curves, like this:

public function drawUniformCubicPath(points:Array, handleLengthRatio:Number = 3, stipplePattern:uint = 0xFFFFFFFF):void {
   var i:int;
   var first:Point, mid:Point, last:Point, a:Point, b:Point, c:Point, d:Point,  firstLen:Number, midLen:Number,lastLen:Number;
   var vec1:Point, vec2:Point, control1:Point, control2:Point;
   a = points[0];
   current.x = a.x;
   current.y = a.y;
   b = points[1];
   c = points[2]
   first = a.subtract(b);
   firstLen = first.length;
   first.normalize(1.0);
   mid = b.subtract(c);
   midLen = mid.length;
   mid.normalize(1.0);
   vec1 = new Point(-first.x, -first.y);
   vec1.normalize(firstLen / handleLengthRatio);
   vec1.offset(a.x, a.y);
   vec2 = Point.interpolate(first, mid, 0.5);
   vec2.normalize(firstLen / handleLengthRatio);
   control1 = new Point(0, 0);
   control2 = new Point(vec2.x, vec2.y);
   control2.offset(b.x, b.y);
   this.cubicCurveToPoint(vec1,control2,b,stipplePattern);
   for (i = 3; i < points.length; i++) {
    d = points;
    last = c.subtract(d);
    lastLen = last.length;
    last.normalize(1.0);
    vec1 = vec2;
    vec1.x = -vec1.x; vec1.y = -vec1.y;
    vec1.normalize(midLen / handleLengthRatio /** ratio*/);
    vec2 = Point.interpolate(mid, last, 0.5);
    vec2.normalize(midLen / handleLengthRatio /** (1.0 - ratio)*/);
    control1.x = vec1.x;
    control1.y = vec1.y;
    control1.offset(b.x, b.y);
    control2.x = vec2.x;
    control2.y = vec2.y;
    control2.offset(c.x, c.y);
    this.cubicCurveToPoint(control1, control2, c, stipplePattern);
    a = b;
    b = c;
    c = d;
    first = mid;
    mid = last;
    firstLen = midLen;
    midLen = lastLen;
   }
   vec2.x = -vec2.x;
   vec2.y = -vec2.y;
   vec2.offset(b.x, b.y);
   mid.normalize(midLen / handleLengthRatio);
   mid.offset(c.x, c.y);
   this.cubicCurveToPoint(vec2,mid,c,stipplePattern);
  }
public function cubicCurveToPoint(control1:Point, control2:Point, anchor:Point, stipplePattern:uint = 0xFFFFFFFF):void {
   var cx:Number = 3 * (control1.x - current.x);
   var bx:Number = 3 * (control2.x - control1.x) - cx;
   var ax:Number = anchor.x - current.x - cx - bx;

   var cy:Number = 3 * (control1.y - current.y);
   var by:Number = 3 * (control2.y - control1.y) - cy;
   var ay:Number = anchor.y - current.y - cy - by;

   var xPos:Number;
   var yPos:Number;
   
   var tSquared:Number = 0;
   var tCubed:Number = 0;
   
   var step:Number = 1.0 / Math.max(Math.abs(current.x-anchor.x),Math.abs(current.y-anchor.y));
   var phase:uint = 0;
   var move:Boolean = false;
   
   for (var t:Number = 0; t <= 1; t += step) {
    //skip the blank spots as defined by the dashes binary type
    while ((stipplePattern & (1 << (31-phase))) == 0) {
     move = true;
     t += step;
     phase = (phase + 1) % 32;
    }
    tSquared = t * t;
    tCubed = tSquared * t;
    xPos = ax * tCubed + bx * tSquared + cx * t + current.x;
    yPos = ay * tCubed + by * tSquared + cy * t + current.y;
    if(move)
     graphics.moveTo(xPos, yPos);     
    else
     graphics.lineTo(xPos, yPos);
    move = false;
    phase = (phase + 1) % 32;
   }
   
   current.x = anchor.x;
   current.y = anchor.y;
  }


I have this in my own graphics wrapper, so current is a local Point variable. I just use it to keep track of the point where the last drawWhatever function finished.

Anyway, you can draw these points then however you like and use your own Symbol. For example, I used something like this to grade the lines (where the color/alpha/thickness changes from the beginning of a linestring to its end).
0 Kudos