/**** * * Isolining package for AS3 * * @author Zachary Forest Johnson (indiemaps.com/blog or zach.f.johnson@gmail.com) * @date June 2008 * * requires my delaunay package (available here: http://indiemaps.com/code/delaunay.zip) * * uh oh, now also requires Andy Woodruff's cubicBezier class (available here: http://cartogrammar.com/source/CubicBezier.as) * * all required packages / classes should be found wherever you found this file * * an indiemaps production, infinitely muggable division * * use as you will! * ****/ package com.indiemaps.isolines { import com.indiemaps.delaunay.*; import com.cartogrammar.drawing.CubicBezier; import flash.display.DisplayObjectContainer; import flash.geom.Point; import flash.display.Sprite; import flash.display.Shape; import flash.text.TextField; import flash.text.TextFormat; import fl.motion.BezierSegment; public class IsoUtils { public function IsoUtils() { // constructor function has no code // class is a container for static methods } /* public static method isoline calls other methods on inputted triangular mesh returns an array of isolines */ public static function isoline(tris:Array, points:Array, clip:DisplayObjectContainer, interval:Number, origin:Number=0):Array { //first, get a nonredundant array of all the edges var edges:Array = IsoUtils.getEdges(tris); /* then, interpolate values along each edge according to the chosen interval and origin this will attach the interpolated points directly to the edge objects themselves */ findInterVals(edges, points, interval, origin); //then, string the actual isolines var isos:Array = stringIsolines(edges, tris, points); //returns an array of isolines //return the array of isolines return isos; } /* public static method getEdges returns a nonredundant array of all edges in a triangulation takes as input a triangular mesh (an array of triangles created by triangulate method) *** HOPEFULLY this method won't be necessary soon *** NEED to find some way of generating this more efficiently directly from the Delaunay.triangulate method *** CURRENTLY this takes WAY longer than the triangulation itself *** THIS is the bottleneck in isolining */ public static function getEdges(v:Array):Array { var allEdges = new Array(); //array to hold all edges nonredundantly for (var trinum in v) { //loop through each triangle var tri = v[trinum]; v[trinum].eArray = new Array(); var eArray:Array = new Array(); eArray.push(new IEdge(tri.p1, tri.p2)); eArray.push(new IEdge(tri.p2, tri.p3)); eArray.push(new IEdge(tri.p3, tri.p1)); for (var enum in eArray) { var e = eArray[enum]; var inIt = false; for (var edgenum in allEdges) { var edge = allEdges[edgenum]; if ((edge.p1 == e.p1 && edge.p2 == e.p2) || (edge.p1 == e.p2 && edge.p2 == e.p1)) { v[trinum].eArray.push(edgenum); allEdges[edgenum].numTris += 1; allEdges[edgenum].tris.push(trinum); inIt = true; break; } } if (!inIt) { v[trinum].eArray.push(allEdges.length); allEdges.push(e); allEdges[allEdges.length-1].numTris += 1; allEdges[allEdges.length-1].tris = new Array(); allEdges[allEdges.length-1].tris.push(trinum); } } } return allEdges; } /* static method findInterVals interpolates critical points along all edges that fall on the interval specified the number inputted is the interval, ex: .01, .1, 1, 10, 100, 7 takes an array of IEdges also requires the original array of points b/c the array of ITriangles simply references key positions in the points array ***should also allow for an origin (like 5 degree contour interval, but starting at 32 degrees or something, rather than zero) */ static function findInterVals(edges:Array, points:Array, interval:Number, origin:Number=0, clipper=null):void { if (clipper != null) { var intClip = new Sprite(); } for (var enum in edges) { //for each edge in the array //call the method to interpolate values along an edge IsoUtils.pointsOnEdge(edges[enum], points, interval, origin); } //and don't return a goddamned thing } /* private static method pointsOnEdge returns an array of the coordinates and value (an XYZ object) of any found interval values uses strict linear interpolation */ private static function pointsOnEdge(edge:IEdge, points:Array, interval:Number, origin:Number=0):void { var p1 = points[edge.p1]; // p1 of this edge var p2 = points[edge.p2]; // p2 of this edge // first, determine which has the greater 'x' of p1 and p2 (b/c this changes how we calculate slope) if (p2.x > p1.x) { var startPt = p1; var currVal = p1.z; var slope = ((p2.y - p1.y) / (p2.x - p1.x)); } else { var startPt = p2; var currVal = p2.z; var slope = ((p1.y - p2.y) / (p1.x - p2.x)); } if (slope > 0) { var angle = Math.asin(slope/(Math.sqrt(slope*slope + 1))); // the angle of A var ymulti = -1; } else { var angle = (2 * Math.PI) - Math.asin(slope/(Math.sqrt(slope*slope + 1))); // the angle of A var ymulti = 1; } var dist; // will hold how far away from either p1 or p2 the next interpolated point is var currPt = new XYZ(); var lineLength = Point.distance(new Point(p1.x, p1.y), new Point(p2.x, p2.y)); // stores the total line length of the edge (the hypotenuse, then) //now let's get ridiculous var curr = (p1.z < p2.z) ? p1.z : p2.z; //curr is the current value (starts as the lower of the two line node values) var end = (p2.z > p1.z) ? p2.z : p1.z; //end is the end value //now find the first interpolated point on the edge segment var currInt = curr + (interval - (((curr / interval) - Math.floor(curr/interval)) * interval)); /******* **** lame Flash rounding errors (54.99999999999 instead of 55) are screwing this part up **** better way of doing this, but for now, I'm just rounding the number if the interval is an integer **** SO, if your interval is not an integer, this rounding error thing may screw you up **** my solution is lame -- make a better one *******/ if (interval is int) currInt = Math.round(currInt); currInt = Math.ceil(currInt / interval) * interval; edge.interPoints = new Array(); while (currInt <= end) { dist = (Math.abs(currInt - currVal) / Math.abs(p1.z - p2.z)) * lineLength; //dist = 10; //var tPoint = Point.polar(dist, angle); currPt.x = startPt.x + (dist * Math.cos(angle)); currPt.y = startPt.y - (dist * Math.sin(angle)) * ymulti; //create the new Interpolated Point object var tempt = new InterpolatedPoint(currPt.x, currPt.y, currInt); //and add the thing to the edge's array edge.interPoints['pt' + currInt] = tempt; //then get next interval value from current position currInt += interval; } } /* temporary debugging function labelTris labels each tri at its bounding box center labels with the triangle ID */ private static function labelTris(tris:Array, points:Array, clip:DisplayObjectContainer) { for (var a in tris) { var xxx = new TextField(); xxx.text = a; xxx.x = .5 * (Math.max(points[tris[a].p1].x, points[tris[a].p2].x, points[tris[a].p3].x) - Math.min(points[tris[a].p1].x, points[tris[a].p2].x, points[tris[a].p3].x)) + Math.min(points[tris[a].p1].x, points[tris[a].p2].x, points[tris[a].p3].x); xxx.y = .5 * (Math.max(points[tris[a].p1].y, points[tris[a].p2].y, points[tris[a].p3].y) - Math.min(points[tris[a].p1].y, points[tris[a].p2].y, points[tris[a].p3].y)) + Math.min(points[tris[a].p1].y, points[tris[a].p2].y, points[tris[a].p3].y); xxx.y = xxx.y * -1; xxx.textColor = 0xff0000; clip.addChild(xxx); } } /* private static method drawIsolines takes output of stringIsolines method WILL eventually include many options (hypsometric tinting, index lines, color, thickness, etc.) */ public static function drawIsolines(lines:Array, clip:DisplayObjectContainer, curveStyle:String="none", colorArray:Array = null, classesArray:Array = null, z=.5, angleFactor=.75) { var ilines = new Sprite(); clip.addChild(ilines); var g = ilines.graphics; for each(var line in lines) { var ptnum; if (colorArray!=null && classesArray!=null && colorArray.length == (classesArray.length+1)) { //if we have viable color and class arrays, go ahead with the line tinting var val = Number(line.val.substr(2)); if (val < classesArray[0]) { var classs=0; } else if (val >= classesArray[classesArray.length-1]) { var classs=colorArray.length-1; } else { for (var cnum=1; cnum < classesArray.length; cnum++) { if (val < classesArray[cnum]) { var classs = cnum; break; } } } g.lineStyle(0, colorArray[classs]); //line thickness set to zero to get out of scaling issues } else { // otherwise, let's just make these lines red g.lineStyle(0, 0xff0000); } switch (curveStyle) { //what kind of scaling do we want, none (rigid), simple (built-in curveTo), or Woody Woodruff's cubicBezier continuous method case "none": g.moveTo(line.coords[0].x, -line.coords[0].y); for (ptnum=1; ptnum