Search This Blog

Wednesday 11 December 2013

My "live" Sonar scan in HTML5/Javascript





I had a desire to have a live sonar feed displayed in the control panel.  Visual feedback, if you will.   At first, I used google charts, and updated it frequently, but it wasn't quite what I was looking for.

The radar chart is not specifically tied to polar coordinates, and it was difficult to accurately represent my sonar scan dataset without fiddling with it too much before sending off to google for charting.  And god forbid I should change the scan interval.....

So... I started looking for a reasonable canvas based Sonar/Radar screen... and I looked... and I looked...  not much out there... Lots of stuff done in "processing" or various toolsets... but no raw HTML5/Javascript canvas.

But I *did* come across this awesome looking HTML5 Clock that was just what I needed as a base to build upon at http://saturnboy.com/2013/10/html5-sonarcnv-clock 




The sonar.php  file which displays the instrument expects it's data as a JSON encoded data set in the form of:
{"ardtime":"43942","pan":"25","radius":"117","heading":"38.6"}
  • Ardtime is the millis counter sent from the Arduino at the time of start of scan.  a UID if you like, to associate a set of data.
  • Pan is the angle at which the sonar pod is taking its reading with respect to the front of the rover.
  • Radius is the distance to target in Centimeters.
  • Heading is the compass direction the rover is pointing.

Currently it clears the face at the end of each sweep.  When I get more time, I will be correcting that as well as integrating multiple overlapping scans to filter out noise.   Yes... I am already normalizing each scan by averaging five pings per position, but still finding spurious reflections that are messing with the mapping.


Here is the "get_sonar.php" script that retrieves the most recent sonar dataset from MySQL and JSON encodes it for the Javascript that draws the sonar screen.





<?php
if (isset($argv)) {            // If running from the command line
    $pan = $argv[1];

}
else {
    $pan = $_GET['pan'];
}
 
 include 'database_creds.inc';

$limit = 360;            // How many results to show in chart
$send_string = '{"success": true,"scan": [';

try {
    $dbh = new PDO("mysql:host=$hostname;dbname=mapdb", $username, $password);
    /*** echo a message saying we have connected ***/
   // echo 'Connected to database<br>';

    $sth = $dbh->query('SELECT MAX(uid) FROM scanning');   // Get UID of most recent scan
    $maxuid = $sth->fetchColumn();
   

     $sql = 'SELECT ardtime FROM scanning WHERE uid = :maxuid';
        $sth = $dbh->prepare($sql);
    $sth->bindValue(':maxuid', $maxuid, PDO::PARAM_INT);
    $sth->execute();
        $row = $sth->fetch(PDO::FETCH_ASSOC);
        $ardtime = $row["ardtime"];

     $sql = 'SELECT ardtime, pan, radius, heading FROM scanning WHERE ardtime = :ardtime ORDER BY pan';
 
    $sth = $dbh->prepare($sql);
    $sth->bindValue(':ardtime', $ardtime, PDO::PARAM_STR);
    //$sth->bindValue(':pan', $pan, PDO::PARAM_STR);
    $sth->execute();

    while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {

                $send_string .= json_encode($row) .',';
    }


     $send_string = rtrim($send_string, ',');
    $send_string .=   '] }';


    echo $send_string;

    $dbh = null;
    $sth = null;


}
catch(PDOException $e)
    {
    echo $e->getMessage();
    }

?>

And here is the "sonar.php" script that displays the most recent sonar dataset as a sweeping sonar screen.



<!DOCTYPE HTML>
<html>
<head>
<title>Sonar</title>
<style>

#sonarcnv {
  position:absolute;
  top:50%;
  left:50%;
  width:400px;
  height:400px;
  margin:-200px 0 0 -200px;
}
</style>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js" type="text/javascript"></script>

</head>

<body onload="sonar();">

<canvas id="sonarcnv"></canvas>

<script>
// Much of this borrowed from  http://saturnboy.com/2013/10/html5-sonarcnv-clock/ 
//
//        
// *******  Global variables    *******
var JSONping = "";          // JSON array of the most recent sonar scan
var secAngle = 0;           // angle of the sweep hand
var ardtime = 0;            // Arduino time (UID) associated with most recent scan

function sonar() {
  var sonarcnv = document.getElementById('sonarcnv');

  //guarantee sonarcnv is supported
  if (!sonarcnv.getContext) {
    alert('sonarcnv not supported!');
    return;
  }
 
  sonarcnv.width = 400;
  sonarcnv.height = 400;
  var sonarctx = sonarcnv.getContext('2d');
  sonarctx.clearRect(0, 0, 400, 400);

  drawFace(sonarctx);

  tick();
}

function tick() {           //  Sweep the hand around the dial

    secAngle += 40 * Math.PI / 60;         
        // 40 is an arbitrary number I chose for the speed of the sweep
   
           var sonarcnv = document.getElementById('sonarcnv');
      var sonarctx = sonarcnv.getContext('2d');
 
      sonarctx.translate(200, 200);
    drawTicks(sonarctx, secAngle);
    drawSweepHand(sonarctx, secAngle);
    sonarctx.translate(-200, -200);
    drawFace2(sonarctx);

    drawPings(sonarctx, secAngle);              // Draw most recent sonar sweep targets
 
      setTimeout(tick, 15);
}

function drawFace(sonarctx) {
    //outer black ring
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 200, 0, 2 * Math.PI);
      sonarctx.fillStyle = '#111';
      sonarctx.fill();
      sonarctx.closePath();
     
      //next light grey ring
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 198, 0, 2 * Math.PI);
      sonarctx.fillStyle = '#bbb';
      sonarctx.fill();
      sonarctx.closePath();


      //outer gradient bezel
      var g1x1 = 200 + 196 * Math.cos(4/3*Math.PI);
      var g1y1 = 200 + 196 * Math.sin(4/3*Math.PI)
      var g1x2 = 200 + 196 * Math.cos(1/3*Math.PI);
      var g1y2 = 200 + 196 * Math.sin(1/3*Math.PI);     
      var g1 = sonarctx.createLinearGradient(g1x1,g1y1,g1x2,g1y2);
      g1.addColorStop(0, '#f4f4f4');  
      g1.addColorStop(1, '#000');
     
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 196, 0, 2 * Math.PI);
      sonarctx.fillStyle = g1;
      sonarctx.fill();
      sonarctx.closePath();

      //next outer bezel
      var g2x1 = 200 + 174 * Math.cos(4/3*Math.PI);
      var g2y1 = 200 + 174 * Math.sin(4/3*Math.PI)
      var g2x2 = 200 + 174 * Math.cos(1/3*Math.PI);
      var g2y2 = 200 + 174 * Math.sin(1/3*Math.PI);     
      var g2 = sonarctx.createLinearGradient(g2x1,g2y1,g2x2,g2y2);
      g2.addColorStop(0, '#000');  
      g2.addColorStop(1, '#777');  
     
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 174, 0, 2 * Math.PI);
      sonarctx.fillStyle = g2;
      sonarctx.fill();
      sonarctx.closePath();

      //tan clock face
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 162, 0, 2 * Math.PI);
      //sonarctx.fillStyle = '#d5c595';
          sonarctx.fillStyle = '#666666';
      sonarctx.fill();
      sonarctx.closePath();

      sonarctx.beginPath();
      sonarctx.arc(200, 200, 38, 0, 2 * Math.PI);
      sonarctx.lineWidth = 2;
          sonarctx.strokeStyle = '#666666';
          sonarctx.stroke();
      sonarctx.closePath();

}

function drawTicks(sonarctx, ang) {
      
  //radial lines
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 0);
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 30);
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 60);
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 90);
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 120);
    drawPath(sonarctx, 'M -98,-1 L 98,-1 98,1 -98,1 Z', '#baa77c', 150);

    //triangle ticks
      drawPath(sonarctx, 'M 154,0 L 178,-6 178,6 Z', '#111', 0);
      drawPath(sonarctx, 'M 154,0 L 178,-6 178,6 Z', '#111', 90);
      drawPath(sonarctx, 'M 154,0 L 178,-6 178,6 Z', '#111', 180);
      drawPath(sonarctx, 'M 154,0 L 178,-6 178,6 Z', '#111', -90);
   
        //long brown ticks 
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', 30);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', 60);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', 120);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', 150);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', -30);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', -60);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', -120);
    drawPath(sonarctx, 'M 156,-2 L 180,-2 180,2 156,2 Z', '#baa77c', -150);
   
    //text labels: 3,6,9,12
    sonarctx.font = '16pt Georgia';
    sonarctx.textAlign = 'center';
    sonarctx.textBaseline = 'middle';
    sonarctx.fillStyle = '#444';

    sonarctx.fillText('30', 0, -48);
    sonarctx.fillText('75', 0, -82);
    sonarctx.fillText('150', 0, -125);
    sonarctx.fillText('225', 0, -155);

    //big dot above 12
    sonarctx.beginPath();
     sonarctx.arc(0, -185, 4, 0, 2 * Math.PI);
      sonarctx.fillStyle = '#444';
     sonarctx.fill();
     sonarctx.closePath();

       
        for (var i = 0; i < 360; i += 30) {
        //outer tick squares
        drawPath(sonarctx, 'M 170,-3 L 174,-3 174,3 170,3 Z', 'rgba(68,68,68,0.8)', i);
       
        //inner tick squares
        drawPath(sonarctx, 'M 104,-3 L 110,-3 110,3 104,3 Z', 'rgba(68,68,68,0.8)', i);

        //outer text labels
        var lbl = '' + Math.round(i +90);
        if (lbl == '360') lbl = '';
        if (lbl == '390') lbl = '30';
        if (lbl == '420') lbl = '60';
        var x = 185 * Math.cos(i * Math.PI / 180.0);
        var y = 185 * Math.sin(i * Math.PI / 180.0);
        sonarctx.save();
        sonarctx.translate(x,y);
        sonarctx.rotate((i + 90 - (i > 0 && i < 180 ? 180 : 0)) * Math.PI / 180.0);
        sonarctx.font = '9pt Georgia';
        sonarctx.textAlign = 'center';
        sonarctx.textBaseline = 'middle';
        sonarctx.fillStyle = 'rgba(68,68,68,0.8)';
        sonarctx.fillText(lbl, 0, 0);
        sonarctx.restore();
       
        //far outer dots between labels
        sonarctx.beginPath();
        x = 180 * Math.cos((i+15) * Math.PI / 180);
        y = 180 * Math.sin((i+15) * Math.PI / 180);
        sonarctx.arc(x, y, 1.5, 0, 2 * Math.PI);
        sonarctx.fillStyle = '#444';
        sonarctx.fill();
        sonarctx.closePath();
                 
        for (var j = 0; j < 25; j += 6) {
            if (j != 0) {
              //outer tick ring - long ticks
                drawPath(sonarctx, 'M 154,-0.5 L 164,-0.5 164,0.5 154,0.5 Z', '#444', i+j);
               
                //inner tick ring - short ticks
                drawPath(sonarctx, 'M 104,-0.5 L 110,-0.5 110,0.5 104,0.5 Z', 'rgba(68,68,68,0.8)', i+j);
            }
           
            for (var k = 1.5; k < 5; k += 1.5) {
                //outer tick ring - short ticks
                drawPath(sonarctx, 'M 160,-0.3 L 164,-0.3 164,0.3 160,0.3 Z', 'rgba(68,68,68,0.5)', i+j+k);
            }
        }
    }
}

function drawSweepHand(sonarctx,ang) {
  drawPath(sonarctx, 'M -50,0 L -45,-5 -25,-5 -22,-2 22,-2 25,-5 160,0 25,5 22,2 -22,2 -25,5 -45,5 Z', '#006666', ang-90);
  // the -90 puts North at the top.
  sonarctx.beginPath();
  sonarctx.arc(0, 0, 8, 0, 2 * Math.PI);
  sonarctx.fillStyle = '#008800';
  sonarctx.fill();
  sonarctx.closePath();
}


function drawFace2(sonarctx) {
  //outer center button ring
  var g1x1 = 200 + 5 * Math.cos(4/3*Math.PI);
  var g1y1 = 200 + 5 * Math.sin(4/3*Math.PI)
  var g1x2 = 200 + 5 * Math.cos(1/3*Math.PI);
  var g1y2 = 200 + 5 * Math.sin(1/3*Math.PI);     
  var g1 = sonarctx.createLinearGradient(g1x1,g1y1,g1x2,g1y2);
  g1.addColorStop(0, '#999');  
  g1.addColorStop(1, '#333');
 
  sonarctx.beginPath();
  sonarctx.arc(200, 200, 5, 0, 2 * Math.PI);
  sonarctx.fillStyle = g1;
  sonarctx.fill();
  sonarctx.closePath();
 
  //inner center button
  var g2x1 = 200 + 3 * Math.cos(4/3*Math.PI);
  var g2y1 = 200 + 3 * Math.sin(4/3*Math.PI)
  var g2x2 = 200 + 3 * Math.cos(1/3*Math.PI);
  var g2y2 = 200 + 3 * Math.sin(1/3*Math.PI);     
  var g2 = sonarctx.createLinearGradient(g2x1,g2y1,g2x2,g2y2);
  g2.addColorStop(0, '#ccc');  
  g2.addColorStop(1, '#aaa');
 
  sonarctx.beginPath();
  sonarctx.arc(200, 200, 3, 0, 2 * Math.PI);
  sonarctx.fillStyle = g2;
  sonarctx.fill();
  sonarctx.closePath();
 
  //highlight (gradient overlay)
  var g3x1 = 200 + 162 * Math.cos(4/3*Math.PI);
  var g3y1 = 200 + 162 * Math.sin(4/3*Math.PI)
  var g3x2 = 200 + 162 * Math.cos(1/3*Math.PI);
  var g3y2 = 200 + 162 * Math.sin(1/3*Math.PI);     
  var g3 = sonarctx.createLinearGradient(g3x1,g3y1,g3x2,g3y2);
  g3.addColorStop(0, 'rgba(0,0,0,0.5)');  
  g3.addColorStop(1, 'rgba(0,0,0,0)');
 
  sonarctx.beginPath();
  sonarctx.arc(200, 200, 162, 0, 2 * Math.PI);
  sonarctx.fillStyle = g3;
  //sonarctx.fillStyle = 'black';
  sonarctx.fill();
  sonarctx.closePath();
 
    sonarctx.beginPath();
    sonarctx.arc(200, 200, 68, 0, 2 * Math.PI);
    sonarctx.lineWidth = 2;
    sonarctx.strokeStyle = '#666666';
    sonarctx.stroke();
    sonarctx.closePath();
   
      sonarctx.beginPath();
      sonarctx.arc(200, 200, 38, 0, 2 * Math.PI);
      sonarctx.lineWidth = 2;
      sonarctx.strokeStyle = '#666666';
      sonarctx.stroke();
      sonarctx.closePath();


}

/** Simple svg path parser that only understands M (move to) and L (line to). */
function drawPath(sonarctx,path,fill,ang) {
  sonarctx.save();
    sonarctx.rotate(ang == undefined ? 0 : ang  * Math.PI / 180.0);
    sonarctx.beginPath();
   
    var parts = path.split(' ');
    while (parts.length > 0) {
      var part = parts.shift();
      if (part == 'M') {
        coords = parts.shift().split(',');
        sonarctx.moveTo(coords[0], coords[1]);
      } else if (part == 'L') {
        continue;
      } else if (part == 'Z') {
        break;
      } else if (part.indexOf(',') >= 0) {
            coords = part.split(',');
            sonarctx.lineTo(coords[0], coords[1]);
        }
    }
   
    sonarctx.closePath();
    sonarctx.fillStyle = (fill == undefined ? '#000' : fill);
    sonarctx.fill();
    sonarctx.restore();
}

function drawPings(sonarctx, ang) {             // Draw sonar targets on dial
var x=null;
var y=null;
var oldx =200, oldy=200;
var rang = findReferenceAngle(ang-90);
var pan = null;
var radius = null;
var heading = null;
var surl = null;
var dt = new Date();
var grabbed = 0;

    if(rang > 0 && rang < 5 && grabbed === 0) {
        grabbed = 1;
         $.get('get_sonar.php', function(row){       // Get the most recent sonar sweep to display
                  JSONping = JSON.parse(row);
        });
    }
    if(dt.getSeconds() > 0) grabbed = 0;

        for (var i in JSONping.scan) {

            var scan = JSONping.scan[i];
            pan = JSONping.scan[i].pan;             // Pan position of sonar scan
            radius = JSONping.scan[i].radius;       // Polar radius
            heading = JSONping.scan[i].heading;
            ardtime = JSONping.scan[i].heading;

        // Red dot for current heading
              sonarctx.beginPath();
              hx = (156 * Math.cos(toRadians(heading-90))) +200;
              hy = (156 * Math.sin(toRadians(heading-90))) + 200;

              sonarctx.arc(hx, hy, 5, 0, 2 * Math.PI);
              sonarctx.fillStyle = "red";
              sonarctx.fill();
              sonarctx.closePath();
  
   
   
            if(radius > 150) radius = 150;  // limit scale to boundary of dial
            // Convert polar to cartesian

            if((pan) <= findReferenceAngle(ang))
            {   
         x = (radius * Math.cos(toRadians(pan-90))) +200;
         y = (radius * Math.sin(toRadians(pan-90))) + 200;

              // Draw the ping target at the cartesian coordinate
              sonarctx.beginPath();
              sonarctx.arc(x, y, 3, 0, 2 * Math.PI);
              sonarctx.fillStyle = "green";
              sonarctx.fill();
              sonarctx.closePath();
              sonarctx.strokeStyle = 'green';
              sonarctx.moveTo(oldx, oldy);
              sonarctx.lineTo(x,y);
              oldx = x; oldy = y;
              sonarctx.stroke();
           }
        }
}


function toDegrees (angle) {
  return angle * (180 / Math.PI);
}

function toRadians (angle) {
  return angle * (Math.PI / 180);
}

function findReferenceAngle(ang) {
var quad = "";
var ref = "No";
    while (ref === "No"){ 
        if (ang >= 0 && ang < 360){     
            ref = ang; 
        } else if (ang < 0){     
            ang = ang + 360; 
        } else {     
            ang = ang - 360; 
        }
    }
    return ang;
}

function print_r(theObj){
  if(theObj.constructor == Array ||
     theObj.constructor == Object){
    document.write("<ul>")
    for(var p in theObj){
      if(theObj[p].constructor == Array||
         theObj[p].constructor == Object){
document.write("<li>["+p+"] => "+typeof(theObj)+"</li>");
        document.write("<ul>")
        print_r(theObj[p]);
        document.write("</ul>")
      } else {
document.write("<li>["+p+"] => "+theObj[p]+"</li>");
      }
    }
    document.write("</ul>")
  }
}
</script>
</body>
</html>




and just for completion, here is what a live dataset, JSON encoded from MYSQL would look like:


{"success": true,"scan": [
{"ardtime":"134","pan":"5","radius":"63","heading":"19.5"},{"ardtime":"134","pan":"10","radius":"74","heading":"19.5"},{"ardtime":"134","pan":"15","radius":"73","heading":"19.5"},{"ardtime":"134","pan":"20","radius":"76","heading":"19.5"},{"ardtime":"134","pan":"25","radius":"69","heading":"19.5"},{"ardtime":"134","pan":"30","radius":"61","heading":"19.5"},{"ardtime":"134","pan":"35","radius":"67","heading":"19.5"},{"ardtime":"134","pan":"40","radius":"61","heading":"19.5"},{"ardtime":"134","pan":"45","radius":"53","heading":"19.5"},{"ardtime":"134","pan":"50","radius":"54","heading":"19.5"},{"ardtime":"134","pan":"55","radius":"43","heading":"19.5"},{"ardtime":"134","pan":"60","radius":"53","heading":"19.5"},{"ardtime":"134","pan":"65","radius":"54","heading":"19.5"},{"ardtime":"134","pan":"70","radius":"56","heading":"19.5"},{"ardtime":"134","pan":"75","radius":"56","heading":"19.5"},{"ardtime":"134","pan":"80","radius":"41","heading":"19.5"},{"ardtime":"134","pan":"85","radius":"58","heading":"19.5"},{"ardtime":"134","pan":"95","radius":"61","heading":"19.6"},{"ardtime":"134","pan":"100","radius":"40","heading":"19.6"},{"ardtime":"134","pan":"105","radius":"50","heading":"19.6"},{"ardtime":"134","pan":"110","radius":"58","heading":"19.6"},{"ardtime":"134","pan":"115","radius":"48","heading":"19.6"},{"ardtime":"134","pan":"120","radius":"59","heading":"19.6"},{"ardtime":"134","pan":"125","radius":"87","heading":"19.6"},{"ardtime":"134","pan":"130","radius":"66","heading":"19.6"},{"ardtime":"134","pan":"135","radius":"75","heading":"19.6"},{"ardtime":"134","pan":"140","radius":"85","heading":"19.6"},{"ardtime":"134","pan":"145","radius":"97","heading":"19.6"},{"ardtime":"134","pan":"150","radius":"104","heading":"19.6"},{"ardtime":"134","pan":"155","radius":"68","heading":"19.6"},{"ardtime":"134","pan":"160","radius":"98","heading":"19.6"},{"ardtime":"134","pan":"165","radius":"102","heading":"19.6"},{"ardtime":"134","pan":"170","radius":"80","heading":"19.6"},{"ardtime":"134","pan":"175","radius":"86","heading":"19.6"},{"ardtime":"134","pan":"180","radius":"102","heading":"19.5"},{"ardtime":"134","pan":"185","radius":"81","heading":"19.5"},{"ardtime":"134","pan":"190","radius":"101","heading":"19.5"},{"ardtime":"134","pan":"195","radius":"98","heading":"19.5"},{"ardtime":"134","pan":"200","radius":"106","heading":"19.5"},{"ardtime":"134","pan":"205","radius":"69","heading":"19.5"},{"ardtime":"134","pan":"210","radius":"112","heading":"19.5"},{"ardtime":"134","pan":"215","radius":"68","heading":"19.5"},{"ardtime":"134","pan":"220","radius":"49","heading":"19.5"},{"ardtime":"134","pan":"225","radius":"71","heading":"19.5"},{"ardtime":"134","pan":"230","radius":"36","heading":"19.5"},{"ardtime":"134","pan":"235","radius":"30","heading":"19.5"},{"ardtime":"134","pan":"240","radius":"41","heading":"19.5"},{"ardtime":"134","pan":"245","radius":"40","heading":"19.5"},{"ardtime":"134","pan":"250","radius":"35","heading":"19.5"},{"ardtime":"134","pan":"255","radius":"46","heading":"19.5"},{"ardtime":"134","pan":"260","radius":"42","heading":"19.5"},{"ardtime":"134","pan":"265","radius":"53","heading":"19.5"},{"ardtime":"134","pan":"275","radius":"80","heading":"19.6"},{"ardtime":"134","pan":"280","radius":"59","heading":"19.6"},{"ardtime":"134","pan":"285","radius":"70","heading":"19.6"},{"ardtime":"134","pan":"290","radius":"74","heading":"19.6"},{"ardtime":"134","pan":"295","radius":"61","heading":"19.6"},{"ardtime":"134","pan":"300","radius":"74","heading":"19.6"},{"ardtime":"134","pan":"305","radius":"76","heading":"19.6"},{"ardtime":"134","pan":"310","radius":"70","heading":"19.6"},{"ardtime":"134","pan":"315","radius":"89","heading":"19.6"},{"ardtime":"134","pan":"320","radius":"93","heading":"19.6"},{"ardtime":"134","pan":"325","radius":"52","heading":"19.6"},{"ardtime":"134","pan":"330","radius":"77","heading":"19.6"},{"ardtime":"134","pan":"335","radius":"105","heading":"19.6"},{"ardtime":"134","pan":"340","radius":"65","heading":"19.6"},{"ardtime":"134","pan":"345","radius":"91","heading":"19.6"},{"ardtime":"134","pan":"350","radius":"88","heading":"19.6"},{"ardtime":"134","pan":"355","radius":"77","heading":"19.6"},{"ardtime":"134","pan":"360","radius":"74","heading":"19.5"}
] }