Phi: Add Holding Pattern Tool
A simple tool to visualize holding pattern entries
This commit is contained in:
parent
3b44cbd6bd
commit
c295450e7d
4 changed files with 273 additions and 1 deletions
|
@ -2,6 +2,10 @@ define([
|
||||||
'knockout', 'text!./Tools.html'
|
'knockout', 'text!./Tools.html'
|
||||||
], function(ko, htmlString) {
|
], function(ko, htmlString) {
|
||||||
|
|
||||||
|
ko.components.register('Tools/Holding Pattern', {
|
||||||
|
require : 'topics/Tools/Holding'
|
||||||
|
});
|
||||||
|
|
||||||
ko.components.register('Tools/Stopwatch', {
|
ko.components.register('Tools/Stopwatch', {
|
||||||
require : 'topics/Tools/Stopwatch'
|
require : 'topics/Tools/Stopwatch'
|
||||||
});
|
});
|
||||||
|
|
105
webgui/topics/Tools/Holding.html
Normal file
105
webgui/topics/Tools/Holding.html
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<div>
|
||||||
|
<style>
|
||||||
|
.holding-patter-navaid {
|
||||||
|
stroke: none;
|
||||||
|
fill: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-direct {
|
||||||
|
fill: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-teardrop {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-parallel {
|
||||||
|
fill: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-legend {
|
||||||
|
fill: white;
|
||||||
|
font-size: 7px;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-racetrack {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holding-pattern-heading {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: #c0c0c0;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: miter;
|
||||||
|
stroke-dasharray: 2, 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Holding Pattern</legend>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Inbound Track</td>
|
||||||
|
<td><input data-bind="spinner: { value: inboundTrack, spin: inboundTrackSpin }"></td>
|
||||||
|
<td rowspan="3">
|
||||||
|
<div style="width: 300px; height: 300px; padding-left: 5em;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="xMinYMin meet">
|
||||||
|
<g data-bind="attr: { transform: holdingTransform }">
|
||||||
|
<path d="M50,50 l37.58770483143634,13.680805733026748 A40,40, 0 0,1 12.41229516856366,36.31919426697327 z"
|
||||||
|
class="holding-pattern-direct" />
|
||||||
|
<path d="M50,50 l-37.587704831436334,-13.680805733026759 A40,40, 0 0,1 49.99999999999999,10 z"
|
||||||
|
class="holding-pattern-teardrop" />
|
||||||
|
<path d="M50,50 l-9.797174393178826e-15,-40 A40,40, 0 0,1 87.58770483143635,63.68080573302672 z"
|
||||||
|
class="holding-pattern-parallel" />
|
||||||
|
<path d="M 50 50 a 7.5 7.5 0 0 1 15 0 v30 a 7.5 7.5 0 0 1 -15 0 z M50 55 l 2 5 h -4 z M65 75 l -2 -5 h4 z" class="holding-pattern-racetrack" />
|
||||||
|
</g>
|
||||||
|
<g data-bind="attr: { transform: holdingTransform }, visible: nonStandard">
|
||||||
|
<path d="M50,50 l37.58770483143634,-13.680805733026748 A40,40, 0 0,1 12.41229516856366,63.6808057331 z"
|
||||||
|
class="holding-pattern-direct" />
|
||||||
|
<path d="M50,50 l0,-40 A40,40, 0 0,1 87.58770483143635,36.31919426697327 z" class="holding-pattern-teardrop" />
|
||||||
|
<path d="M50,50 l-37.587704831436334,13.680805733026759 A40,40, 0 0,1 50,10 z" class="holding-pattern-parallel" />
|
||||||
|
<path d="M 50 50 a 7.5 7.5 0 0 0 -15 0 v 30 a 7.5 7.5 0 0 0 15 0 z M50 55 l 2 5 h -4 z M35 75 l -2 -5 h4 z" class="holding-pattern-racetrack" />
|
||||||
|
</g>
|
||||||
|
<circle cx="50" cy="50" r="2.5" class="holding-patter-navaid" />
|
||||||
|
|
||||||
|
<g data-bind="attr: { transform: trackTransform }">
|
||||||
|
<path data-bind="attr: { d: trackDraw }" class="holding-pattern-heading" />
|
||||||
|
</g>
|
||||||
|
<rect x="0" y="92" rx="2" ry="2" width="30" height="8" class="holding-pattern-direct" />
|
||||||
|
<text x="15" y="98" class="holding-pattern-legend">direct</text>
|
||||||
|
|
||||||
|
<rect x="35" y="92" rx="2" ry="2" width="30" height="8" class="holding-pattern-teardrop" />
|
||||||
|
<text x="50" y="98" class="holding-pattern-legend">teardrop</text>
|
||||||
|
|
||||||
|
<rect x="70" y="92" rx="2" ry="2" width="30" height="8" class="holding-pattern-parallel" />
|
||||||
|
<text x="85" y="98" class="holding-pattern-legend">parallel</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Heading</td>
|
||||||
|
<td><input data-bind="spinner: { value: heading, spin: headingSpin }"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><input id="holding-type-nonstandard" type="radio" name="holdingtype"
|
||||||
|
data-bind="button: {}, event: { change: setNonStandard }"> <label for="holding-type-nonstandard">Non
|
||||||
|
Standard (Left)</label> <input id="holding-type-standard" type="radio" name="holdingtype" checked="checked"
|
||||||
|
data-bind="button: {}, event: { change: setStandard }"> <label for="holding-type-standard">Standard
|
||||||
|
(Right)</label></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
<!--
|
||||||
|
<div data-bind="text: entry"></div>
|
||||||
|
<div data-bind="text: ko.toJSON($data)"></div>
|
||||||
|
-->
|
||||||
|
</div>
|
163
webgui/topics/Tools/Holding.js
Normal file
163
webgui/topics/Tools/Holding.js
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
define([
|
||||||
|
'jquery', 'knockout', 'text!./Holding.html', 'sprintf', 'kojqui/button', 'kojqui/spinner'
|
||||||
|
], function(jquery, ko, htmlString, sprintf) {
|
||||||
|
|
||||||
|
function ViewModel(params) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function normDeg(val, min, max) {
|
||||||
|
var d = max - min;
|
||||||
|
while (val >= max)
|
||||||
|
val -= d;
|
||||||
|
while (val < min)
|
||||||
|
val += d;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.standard = ko.observable(true);
|
||||||
|
self.nonStandard = ko.pureComputed(function() {
|
||||||
|
return false == self.standard();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.inboundTrack = ko.observable(0);
|
||||||
|
self.heading = ko.observable(270);
|
||||||
|
self.entry = ko.observable("");
|
||||||
|
|
||||||
|
self.entryClass = function(p) {
|
||||||
|
console.log(p, self.entry());
|
||||||
|
if (p == self.entry())
|
||||||
|
return 'holding-pattern-' + p;
|
||||||
|
else
|
||||||
|
return 'active-holding-pattern-' + p;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.holdingTransform = ko.pureComputed(function() {
|
||||||
|
return sprintf.sprintf("rotate(%f 50 50)", self.inboundTrack());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.trackTransform = ko.pureComputed(function() {
|
||||||
|
return sprintf.sprintf("rotate(%f 50 50)", self.heading());
|
||||||
|
});
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
var v = [ -1, 0 ];
|
||||||
|
var phi = 0 * Math.PI/180;
|
||||||
|
|
||||||
|
var cosPhi = Math.cos(phi);
|
||||||
|
var sinPhi = Math.sin(phi);
|
||||||
|
var m = [ [ cosPhi, sinPhi ], [ -sinPhi, cosPhi ] ];
|
||||||
|
|
||||||
|
var r = [ m[0][0] * v[0] + m[1][0]*v[1], m[0][1] * v[0] + m[1][1]*v[1]];
|
||||||
|
|
||||||
|
console.log(v,m,r);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOnArc( targetHeading, r, dir ) {
|
||||||
|
dir = dir || 1;
|
||||||
|
var phi = targetHeading * Math.PI/180;
|
||||||
|
var cosPhi = Math.cos(phi);
|
||||||
|
var sinPhi = Math.sin(phi);
|
||||||
|
var x = dir*r*(1- cosPhi);
|
||||||
|
var y = r * sinPhi;
|
||||||
|
return [ Number(x.toFixed(1)), Number(-y.toFixed(1)) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStraight( heading, dist ) {
|
||||||
|
var phi = heading * Math.PI/180;
|
||||||
|
var cosPhi = Math.cos(phi);
|
||||||
|
var sinPhi = Math.sin(phi);
|
||||||
|
var x = dist * sinPhi;
|
||||||
|
var y = dist * cosPhi;
|
||||||
|
return [ Number(x.toFixed(1)), Number(-y.toFixed(1)) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.trackDraw = ko.pureComputed(function() {
|
||||||
|
function entryProcedure(s, t, h) {
|
||||||
|
var d = normDeg(t - h, -180, 180);
|
||||||
|
var reply = "";
|
||||||
|
|
||||||
|
var dir = s ? 1 : -1;
|
||||||
|
|
||||||
|
if ((s && d >= -110 && d < 70) || (!s && d >= -70 && d < 110)) {
|
||||||
|
self.entry("direct");
|
||||||
|
// turn to outbound track
|
||||||
|
var turn = normDeg(dir*d+180,0,360);
|
||||||
|
var p = moveOnArc(turn, 7.5, dir );
|
||||||
|
reply += sprintf.sprintf(" a 7.5 7.5, 0, %d, %d, %f %f ", turn>180?1:0, s?1:0, p[0], p[1] );
|
||||||
|
|
||||||
|
// fly outbound
|
||||||
|
p = moveStraight(d+180, 25);
|
||||||
|
reply += sprintf.sprintf(" l %f,%f", p[0], p[1] );
|
||||||
|
|
||||||
|
// turn back to the holding pattern, intercept inbound
|
||||||
|
p = moveStraight(d-dir*90, 15 );
|
||||||
|
reply += sprintf.sprintf(" a 7.5 7.5, 0, %d, %d, %f %f ", 0, s?1:0, p[0], p[1] );
|
||||||
|
|
||||||
|
// and to the fix
|
||||||
|
reply += " L50,50";
|
||||||
|
} else if ((s && d >= -180 && d < -110) || (!s && d >= 110 && d < 180)) {
|
||||||
|
self.entry("teardrop");
|
||||||
|
|
||||||
|
// fly outbount for 1minute
|
||||||
|
var p = moveStraight(d+180-dir*30, 30 );
|
||||||
|
reply += sprintf.sprintf(" l %f,%f", p[0], p[1] );
|
||||||
|
|
||||||
|
// turn back to station
|
||||||
|
p = moveOnArc(d-dir*30, 7.5, dir );
|
||||||
|
reply += sprintf.sprintf(" a 7.5 7.5, 0, %d, %d, %f %f ", 0, s?1:0, p[0], p[1] );
|
||||||
|
|
||||||
|
reply += " L50,50";
|
||||||
|
} else if ((s && d >= 70 && d < 180) || (!s && d >= -180 && d < -70)) {
|
||||||
|
self.entry("parallel");
|
||||||
|
// turn to outbound track
|
||||||
|
var turn = normDeg(dir*d+180,0,360);
|
||||||
|
var p = moveOnArc(180-dir*d, 7.5, -dir );
|
||||||
|
reply += sprintf.sprintf(" a 7.5 7.5, 0, %d, %d, %f %f ", 0, s?0:1, p[0], p[1] );
|
||||||
|
|
||||||
|
// fly outbound
|
||||||
|
p = moveStraight(d+180, 25);
|
||||||
|
reply += sprintf.sprintf(" l %f,%f", p[0], p[1] );
|
||||||
|
|
||||||
|
// turn back to the holding pattern, intercept inbound
|
||||||
|
p = moveStraight(d+dir*90, 15 );
|
||||||
|
reply += sprintf.sprintf(" a 7.5 7.5, 0, %d, %d, %f %f ", 0, s?0:1, p[0], p[1] );
|
||||||
|
|
||||||
|
reply += " L50,50";
|
||||||
|
} else {
|
||||||
|
self.entry("unknown");
|
||||||
|
}
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
return "M50,100 v-50 " + entryProcedure(self.standard(), self.inboundTrack(), self.heading());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.setStandard = function(a, b) {
|
||||||
|
self.standard(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setNonStandard = function(a, b) {
|
||||||
|
self.standard(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inboundTrackSpin = function(event, ui) {
|
||||||
|
$(event.target).spinner("value", normDeg(ui.value, 0, 360));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.headingSpin = function(event, ui) {
|
||||||
|
$(event.target).spinner("value", normDeg(ui.value, 0, 360));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModel.prototype.dispose = function() {
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Return component definition
|
||||||
|
return {
|
||||||
|
viewModel : ViewModel,
|
||||||
|
template : htmlString
|
||||||
|
};
|
||||||
|
});
|
|
@ -5,7 +5,7 @@ define([
|
||||||
function ViewModel(params) {
|
function ViewModel(params) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.watches = ko.observableArray([]);
|
self.watches = ko.observableArray([0]);
|
||||||
|
|
||||||
self.addWatch = function() {
|
self.addWatch = function() {
|
||||||
self.watches.push(self.watches().length);
|
self.watches.push(self.watches().length);
|
||||||
|
|
Loading…
Reference in a new issue