-
Chase car mode
+
Chase Mode
Enable
@@ -358,20 +386,23 @@
Report Recovery
-
+
+
+
-
diff --git a/js/skewt.js b/js/skewt.js
new file mode 100644
index 0000000..1838649
--- /dev/null
+++ b/js/skewt.js
@@ -0,0 +1,1783 @@
+(function(){'use strict';// Linear interpolation
+// The values (y1 and y2) can be arrays
+//export
+function linearInterpolate(x1, y1, x2, y2, x) {
+ if (x1 == x2) {
+ return y1;
+ }
+ const w = (x - x1) / (x2 - x1);
+
+ if (Array.isArray(y1)) {
+ return y1.map((y1, i) => y1 * (1 - w) + y2[i] * w);
+ }
+ return y1 * (1 - w) + y2 * w;
+}
+
+// Sampling at at targetXs with linear interpolation
+// xs and ys must have the same length.
+//export
+function sampleAt(xs, ys, targetXs) {
+ const descOrder = xs[0] > xs[1];
+ return targetXs.map((tx) => {
+ let index = xs.findIndex((x) => (descOrder ? x <= tx : x >= tx));
+ if (index == -1) {
+ index = xs.length - 1;
+ } else if (index == 0) {
+ index = 1;
+ }
+ return linearInterpolate(xs[index - 1], ys[index - 1], xs[index], ys[index], tx);
+ });
+}
+
+// x?s must be sorted in ascending order.
+// x?s and y?s must have the same length.
+// return [x, y] or null when no intersection found.
+//export
+function firstIntersection(x1s, y1s, x2s, y2s) {
+ // Find all the points in the intersection of the 2 x ranges
+ const min = Math.max(x1s[0], x2s[0]);
+ const max = Math.min(x1s[x1s.length - 1], x2s[x2s.length - 1]);
+ const xs = Array.from(new Set([...x1s, ...x2s]))
+ .filter((x) => x >= min && x <= max)
+ .sort((a, b) => (Number(a) > Number(b) ? 1 : -1));
+ // Interpolate the lines for all the points of that intersection
+ const iy1s = sampleAt(x1s, y1s, xs);
+ const iy2s = sampleAt(x2s, y2s, xs);
+ // Check if each segment intersect
+ for (let index = 0; index < xs.length - 1; index++) {
+ const y11 = iy1s[index];
+ const y21 = iy2s[index];
+ const x1 = xs[index];
+ if (y11 == y21) {
+ return [x1, y11];
+ }
+ const y12 = iy1s[index + 1];
+ const y22 = iy2s[index + 1];
+ if (Math.sign(y21 - y11) != Math.sign(y22 - y12)) {
+ const x2 = xs[index + 1];
+ const width = x2 - x1;
+ const slope1 = (y12 - y11) / width;
+ const slope2 = (y22 - y21) / width;
+ const dx = (y21 - y11) / (slope1 - slope2);
+ const dy = dx * slope1;
+ return [x1 + dx, y11 + dy];
+ }
+ }
+ return null;
+}
+
+//export
+function zip(a, b) {
+ return a.map((v, i) => [v, b[i]]);
+}
+
+//export
+function scaleLinear(from, to) {
+ const scale = (v) => sampleAt(from, to, [v])[0];
+ scale.invert = (v) => sampleAt(to, from, [v])[0];
+ return scale;
+}
+
+//export
+function scaleLog(from, to) {
+ from = from.map(Math.log);
+ const scale = (v) => sampleAt(from, to, [Math.log(v)])[0];
+ scale.invert = (v) => Math.exp(sampleAt(to, from, [v])[0]);
+ return scale;
+}
+
+//export
+function line(x, y) {
+ return (d) => {
+ const points = d.map((v) => x(v).toFixed(1) + "," + y(v).toFixed(1));
+ return "M" + points.join("L");
+ };
+}
+
+//export
+function lerp(v0, v1, weight) {
+ return v0 + weight * (v1 - v0);
+}
+
+var math = {
+ linearInterpolate,
+ sampleAt,
+ zip,
+ firstIntersection,
+ scaleLinear,
+ scaleLog,
+ line,
+ lerp,
+};// Gas constant for dry air at the surface of the Earth
+const Rd = 287;
+// Specific heat at constant pressure for dry air
+const Cpd = 1005;
+// Molecular weight ratio
+const epsilon = 18.01528 / 28.9644;
+// Heat of vaporization of water
+const Lv = 2501000;
+// Ratio of the specific gas constant of dry air to the specific gas constant for water vapour
+const satPressure0c = 6.112;
+// C + celsiusToK -> K
+const celsiusToK = 273.15;
+const L$1 = -6.5e-3;
+const g = 9.80665;
+
+/**
+ * Computes the temperature at the given pressure assuming dry processes.
+ *
+ * t0 is the starting temperature at p0 (degree Celsius).
+ */
+
+
+
+//export
+function dryLapse(p, tK0, p0) {
+ return tK0 * Math.pow(p / p0, Rd / Cpd);
+}
+
+
+//to calculate isohume lines:
+//1. Obtain saturation vapor pressure at a specific temperature = partial pressure at a specific temp where the air will be saturated.
+//2. Mixing ratio: Use the partial pressure where air will be saturated and the actual pressure to determine the degree of mixing, thus what % of air is water.
+//3. Having the mixing ratio at the surface, calculate the vapor pressure at different pressures.
+//4. Dewpoint temp can then be calculated with the vapor pressure.
+
+// Computes the mixing ration of a gas.
+//export
+function mixingRatio(partialPressure, totalPressure, molecularWeightRatio = epsilon) {
+ return (molecularWeightRatio * partialPressure) / (totalPressure - partialPressure);
+}
+
+// Computes the saturation mixing ratio of water vapor.
+//export
+function saturationMixingRatio(p, tK) {
+ return mixingRatio(saturationVaporPressure(tK), p);
+}
+
+// Computes the saturation water vapor (partial) pressure
+//export
+function saturationVaporPressure(tK) {
+ const tC = tK - celsiusToK;
+ return satPressure0c * Math.exp((17.67 * tC) / (tC + 243.5));
+}
+
+// Computes the temperature gradient assuming liquid saturation process.
+//export
+function moistGradientT(p, tK) {
+ const rs = saturationMixingRatio(p, tK);
+ const n = Rd * tK + Lv * rs;
+ const d = Cpd + (Math.pow(Lv, 2) * rs * epsilon) / (Rd * Math.pow(tK, 2));
+ return (1 / p) * (n / d);
+}
+
+// Computes water vapor (partial) pressure.
+//export
+function vaporPressure(p, mixing) {
+ return (p * mixing) / (epsilon + mixing);
+}
+
+// Computes the ambient dewpoint given the vapor (partial) pressure.
+//export
+function dewpoint(p) {
+ const val = Math.log(p / satPressure0c);
+ return celsiusToK + (243.5 * val) / (17.67 - val);
+}
+
+//export
+function getElevation(p, p0 = 1013.25) {
+ const t0 = 288.15;
+ //const p0 = 1013.25;
+ return (t0 / L$1) * (Math.pow(p / p0, (-L$1 * Rd) / g) - 1);
+}
+
+//export
+function getElevation2(p, refp = 1013.25) { //pressure altitude with NOAA formula (https://en.wikipedia.org/wiki/Pressure_altitude)
+ return 145366.45 * (1 - Math.pow(p / refp, 0.190284)) / 3.28084;
+}
+
+//export
+function pressureFromElevation(e, refp = 1013.25) {
+ e = e * 3.28084;
+ return Math.pow((-(e / 145366.45 - 1)), 1 / 0.190284) * refp;
+}
+
+//export
+function getSurfaceP(surfElev, refElev = 110.8, refP = 1000) { //calculate surface pressure at surfelev, from reference elev and ref pressure.
+ let elevD = surfElev - refElev;
+ return pressureFromElevation(elevD, refP);
+}
+
+//export
+
+/**
+ * @param params = {temp, gh, level}
+ * @param surface temp, pressure and dewpoint
+ */
+
+
+function parcelTrajectory(params, steps, sfcT, sfcP, sfcDewpoint) {
+
+ //remove invalid or NaN values in params
+ for (let i = 0; i < params.temp.length; i++) {
+ let inval = false;
+ for (let p in params) if (!params[p][i] && params[p][i] !== 0) inval = true;
+ if (inval) for (let p in params) params[p].splice(i, 1);
+ }
+
+ const parcel = {};
+ const dryGhs = [];
+ const dryPressures = [];
+ const dryTemps = []; //dry temps from surface temp, which can be greater than templine start
+ const dryDewpoints = [];
+ const dryTempsTempline = []; //templine start
+
+ const mRatio = mixingRatio(saturationVaporPressure(sfcDewpoint), sfcP);
+
+ const pToEl = math.scaleLog(params.level, params.gh);
+ const minEl = pToEl(sfcP);
+ const maxEl = Math.max(minEl, params.gh[params.gh.length - 1]);
+ const stepEl = (maxEl - minEl) / steps;
+
+ const moistLineFromEandT = (elevation, t) => {
+ //this calculates a moist line from elev and temp to the intersection of the temp line if the intersection exists otherwise very high cloudtop
+ const moistGhs = [], moistPressures = [], moistTemps = [];
+ let previousP = pToEl.invert(elevation);
+ for (; elevation < maxEl + stepEl; elevation += stepEl) {
+ const p = pToEl.invert(elevation);
+ t = t + (p - previousP) * moistGradientT(p, t);
+ previousP = p;
+ moistGhs.push(elevation);
+ moistPressures.push(p);
+ moistTemps.push(t);
+ }
+ let moist = math.zip(moistTemps, moistPressures);
+ let cloudTop, pCloudTop;
+ const equilibrium = math.firstIntersection(moistGhs, moistTemps, params.gh, params.temp);
+
+ if (moistTemps.length){
+ let i1 = params.gh.findIndex(e=> e>moistGhs[1]),i2=i1-1;
+ if (i2>0){
+ let tempIp = math.linearInterpolate(params.gh[i1],params.temp[i1],params.gh[i2],params.temp[i2],moistGhs[1]);
+ if (moistTemps[1] < tempIp){
+ if (!equilibrium){
+ //console.log("%c no Equilibrium found, cut moist temp line short","color:green");
+ //no intersection found, so use point one as the end.
+ equilibrium = [moistGhs[1], moistTemps[1]];
+
+ }
+ }
+ }
+ }
+ if (equilibrium) {
+ cloudTop = equilibrium[0];
+ pCloudTop = pToEl.invert(equilibrium[0]);
+ moist = moist.filter((pt) => pt[1] >= pCloudTop);
+ moist.push([equilibrium[1], pCloudTop]);
+ } else { //does not intersect, very high CBs
+ cloudTop = 100000;
+ pCloudTop = pToEl.invert(cloudTop);
+ }
+ return { moist, cloudTop, pCloudTop };
+ };
+
+
+ for (let elevation = minEl; elevation <= maxEl; elevation += stepEl) {
+ const p = pToEl.invert(elevation);
+ const t = dryLapse(p, sfcT, sfcP);
+ const dp = dewpoint(vaporPressure(p, mRatio));
+ dryGhs.push(elevation);
+ dryPressures.push(p);
+ dryTemps.push(t); //dry adiabat line from templine surfc
+ dryDewpoints.push(dp); //isohume line from dewpoint line surfc
+
+ const t2 = dryLapse(p, params.temp[0], sfcP);
+ dryTempsTempline.push(t2);
+ }
+
+ const cloudBase = math.firstIntersection(dryGhs, dryTemps, dryGhs, dryDewpoints);
+ //intersection dry adiabat from surface temp to isohume from surface dewpoint, if dp==surf temp, then cloudBase will be null
+
+ let thermalTop = math.firstIntersection(dryGhs, dryTemps, params.gh, params.temp);
+ //intersection of dryadiabat from surface to templine. this will be null if stable, leaning to the right
+
+ let LCL = math.firstIntersection(dryGhs, dryTempsTempline, dryGhs, dryDewpoints);
+ //intersection dry adiabat from surface temp to isohume from surface dewpoint, if dp==surf temp, then cloudBase will be null
+
+ let CCL = math.firstIntersection(dryGhs, dryDewpoints, params.gh, params.temp);
+ //console.log(CCL, dryGhs, dryDewpoints, params.gh, params.temp );
+ //intersection of isohume line with templine
+
+
+ //console.log(cloudBase, thermalTop, LCL, CCL);
+
+ if (LCL && LCL.length) {
+ parcel.LCL = LCL[0];
+ let LCLp = pToEl.invert(LCL[0]);
+ parcel.isohumeToDry = [].concat(
+ math.zip(dryTempsTempline, dryPressures).filter(p => p[1] >= LCLp),
+ [[LCL[1], LCLp]],
+ math.zip(dryDewpoints, dryPressures).filter(p => p[1] >= LCLp).reverse()
+ );
+ }
+
+ if (CCL && CCL.length) {
+ //parcel.CCL=CCL[0];
+ let CCLp = pToEl.invert(CCL[0]);
+ parcel.TCON = dryLapse(sfcP, CCL[1], CCLp);
+
+ //check if dryTempsTCON crosses temp line at CCL, if lower, then inversion exists and TCON, must be moved.
+
+ //console.log(parcel.TCON)
+ let dryTempsTCON=[];
+
+ for(let CCLtempMoreThanTempLine=false; !CCLtempMoreThanTempLine; parcel.TCON+=0.5){
+
+ let crossTemp = [-Infinity];
+
+ for (; crossTemp[0] < CCL[0]; parcel.TCON += 0.5) {
+ //if (crossTemp[0]!=-Infinity) console.log("TCON MUST BE MOVED");
+ dryTempsTCON = [];
+ for (let elevation = minEl; elevation <= maxEl; elevation += stepEl) { //line from isohume/temp intersection to TCON
+ const t = dryLapse(pToEl.invert(elevation), parcel.TCON, sfcP);
+ dryTempsTCON.push(t);
+ }
+ crossTemp = math.firstIntersection(dryGhs, dryTempsTCON, params.gh, params.temp) || [-Infinity]; //intersection may return null
+
+
+ }
+
+ parcel.TCON -= 0.5;
+
+ if (crossTemp[0] > CCL[0]) {
+ CCL = math.firstIntersection(dryGhs, dryTempsTCON, dryGhs, dryDewpoints);
+ //now check if temp at CCL is more than temp line, if not, has hit another inversion and parcel.TCON must be moved further
+ let i2= params.gh.findIndex(gh => gh>CCL[0]), i1= i2-1;
+ if (i1>=0){
+ let tempLineIp=math.linearInterpolate(params.gh[i1], params.temp[i1], params.gh[i2], params.temp[i2], CCL[0]);
+ if (CCL[1] > tempLineIp) {
+ CCLtempMoreThanTempLine = true;
+ //console.log("%c CCL1 is more than templine", "color:green", CCL[1], tempLineIp);
+ }
+ }
+ }
+ }
+ parcel.TCON -= 0.5;
+
+
+ parcel.CCL = CCL[0];
+ CCLp = pToEl.invert(CCL[0]);
+
+ parcel.isohumeToTemp = [].concat(
+ math.zip(dryDewpoints, dryPressures).filter(p => p[1] >= CCLp),
+ [[CCL[1], CCLp]],
+ math.zip(dryTempsTCON, dryPressures).filter(p => p[1] >= CCLp).reverse()
+ );
+ parcel.moistFromCCL = moistLineFromEandT(CCL[0], CCL[1]).moist;
+ }
+
+ parcel.surface = params.gh[0];
+
+
+ if (!thermalTop) {
+ return parcel;
+ } else {
+ parcel.origThermalTop = thermalTop[0];
+ }
+
+ if (thermalTop && cloudBase && cloudBase[0] < thermalTop[0]) {
+
+ thermalTop = cloudBase;
+
+ const pCloudBase = pToEl.invert(cloudBase[0]);
+
+ Object.assign(
+ parcel,
+ moistLineFromEandT(cloudBase[0], cloudBase[1]) //add to parcel: moist = [[moistTemp,moistP]...], cloudTop and pCloudTop.
+ );
+
+ const isohume = math.zip(dryDewpoints, dryPressures).filter((pt) => pt[1] > pCloudBase); //filter for pressures higher than cloudBase, thus lower than cloudBase
+ isohume.push([cloudBase[1], pCloudBase]);
+
+
+
+ //parcel.pCloudTop = params.level[params.level.length - 1];
+
+
+
+ //parcel.cloudTop = cloudTop;
+ //parcel.pCloudTop = pCloudTop;
+
+ //parcel.moist = moist;
+
+ parcel.isohume = isohume;
+
+ }
+
+ let pThermalTop = pToEl.invert(thermalTop[0]);
+ const dry = math.zip(dryTemps, dryPressures).filter((pt) => pt[1] > pThermalTop);
+ dry.push([thermalTop[1], pThermalTop]);
+
+ parcel.dry = dry;
+ parcel.pThermalTop = pThermalTop;
+ parcel.elevThermalTop = thermalTop[0];
+
+
+
+ //console.log(parcel);
+ return parcel;
+}
+
+var atm = {
+ dryLapse,
+ mixingRatio,
+ saturationVaporPressure,
+ moistGradientT,
+ vaporPressure,
+ dewpoint,
+ getElevation,
+ getElevation2,
+ pressureFromElevation,
+ getSurfaceP,
+ parcelTrajectory,
+};function lerp$1(v0, v1, weight) {
+ return v0 + weight * (v1 - v0);
+}
+/////
+
+
+
+
+const lookup = new Uint8Array(256);
+
+for (let i = 0; i < 160; i++) {
+ lookup[i] = clampIndex(24 * Math.floor((i + 12) / 16), 160);
+}
+
+
+
+// Compute the rain clouds cover.
+// Output an object:
+// - clouds: the clouds cover,
+// - width & height: dimension of the cover data.
+function computeClouds(ad, wdth = 1, hght = 200) { ////added wdth and hght, to improve performance ///supply own hrAlt altutude percentage distribution, based on pressure levels
+ // Compute clouds data.
+
+ //console.log("WID",wdth,hght);
+
+ /////////convert to windy format
+ //ad must be sorted;
+
+ const logscale = (x, d, r) => { //log scale function D3, x is the value d is the domain [] and r is the range []
+ let xlog = Math.log10(x),
+ dlog = [Math.log10(d[0]), Math.log10(d[1])],
+ delta_d = dlog[1] - dlog[0],
+ delta_r = r[1] - r[0];
+ return r[0] + ((xlog - dlog[0]) / delta_d) * delta_r;
+ };
+
+ let airData = {};
+ let hrAltPressure = [], hrAlt = [];
+ ad.forEach(a => {
+ if (!a.press) return;
+ if (a.rh == void 0 && a.dwpt && a.temp) {
+ a.rh = 100 * (Math.exp((17.625 * a.dwpt) / (243.04 + a.dwpt)) / Math.exp((17.625 * a.temp) / (243.04 + a.temp))); ///August-Roche-Magnus approximation.
+ }
+ if (a.rh && a.press >= 100) {
+ let p = Math.round(a.press);
+ airData[`rh-${p}h`] = [a.rh];
+ hrAltPressure.push(p);
+ hrAlt.push(logscale(p, [1050, 100], [0, 100]));
+ }
+ });
+
+ //fi x underground clouds, add humidty 0 element in airData wehre the pressure is surfcace pressure +1:
+ airData[`rh-${(hrAltPressure[0] + 1)}h`] = [0];
+ hrAlt.unshift(null, hrAlt[0]);
+ hrAltPressure.unshift(null, hrAltPressure[0] + 1);
+ hrAltPressure.pop(); hrAltPressure.push(null);
+
+ ///////////
+
+
+ const numX = airData[`rh-${hrAltPressure[1]}h`].length;
+ const numY = hrAltPressure.length;
+ const rawClouds = new Array(numX * numY);
+
+ for (let y = 0, index = 0; y < numY; ++y) {
+ if (hrAltPressure[y] == null) {
+ for (let x = 0; x < numX; ++x) {
+ rawClouds[index++] = 0.0;
+ }
+ } else {
+ const weight = hrAlt[y] * 0.01;
+ const pAdd = lerp$1(-60, -70, weight);
+ const pMul = lerp$1(0.025, 0.038, weight);
+ const pPow = lerp$1(6, 4, weight);
+ const pMul2 = 1 - 0.8 * Math.pow(weight, 0.7);
+ const rhRow = airData[`rh-${hrAltPressure[y]}h`];
+ for (let x = 0; x < numX; ++x) {
+ const hr = Number(rhRow[x]);
+ let f = Math.max(0.0, Math.min((hr + pAdd) * pMul, 1.0));
+ f = Math.pow(f, pPow) * pMul2;
+ rawClouds[index++] = f;
+ }
+ }
+ }
+
+
+ // Interpolate raw clouds.
+ const sliceWidth = wdth || 10;
+ const width = sliceWidth * numX;
+ const height = hght || 300;
+ const clouds = new Array(width * height);
+ const kh = (height - 1) * 0.01;
+ const dx2 = (sliceWidth + 1) >> 1;
+ let heightLookupIndex = 2 * height;
+ const heightLookup = new Array(heightLookupIndex);
+ const buffer = new Array(16);
+ let previousY;
+ let currentY = height;
+
+ for (let j = 0; j < numY - 1; ++j) {
+ previousY = currentY;
+ currentY = Math.round(height - 1 - hrAlt[j + 1] * kh);
+ const j0 = numX * clampIndex(j + 2, numY);
+ const j1 = numX * clampIndex(j + 1, numY);
+ const j2 = numX * clampIndex(j + 0, numY);
+ const j3 = numX * clampIndex(j - 1, numY);
+ let previousX = 0;
+ let currentX = dx2;
+ const deltaY = previousY - currentY;
+ const invDeltaY = 1.0 / deltaY;
+
+ for (let i = 0; i < numX + 1; ++i) {
+ if (i == 0 && deltaY > 0) {
+ const ry = 1.0 / deltaY;
+ for (let l = 0; l < deltaY; l++) {
+ heightLookup[--heightLookupIndex] = j;
+ heightLookup[--heightLookupIndex] = Math.round(10000 * ry * l);
+ }
+ }
+ const i0 = clampIndex(i - 2, numX);
+ const i1 = clampIndex(i - 1, numX);
+ const i2 = clampIndex(i + 0, numX);
+ const i3 = clampIndex(i + 1, numX);
+ buffer[0] = rawClouds[j0 + i0];
+ buffer[1] = rawClouds[j0 + i1];
+ buffer[2] = rawClouds[j0 + i2];
+ buffer[3] = rawClouds[j0 + i3];
+ buffer[4] = rawClouds[j1 + i0];
+ buffer[5] = rawClouds[j1 + i1];
+ buffer[6] = rawClouds[j1 + i2];
+ buffer[7] = rawClouds[j1 + i3];
+ buffer[8] = rawClouds[j2 + i0];
+ buffer[9] = rawClouds[j2 + i1];
+ buffer[10] = rawClouds[j2 + i2];
+ buffer[11] = rawClouds[j2 + i3];
+ buffer[12] = rawClouds[j3 + i0];
+ buffer[13] = rawClouds[j3 + i1];
+ buffer[14] = rawClouds[j3 + i2];
+ buffer[15] = rawClouds[j3 + i3];
+
+ const topLeft = currentY * width + previousX;
+ const dx = currentX - previousX;
+ const fx = 1.0 / dx;
+
+ for (let y = 0; y < deltaY; ++y) {
+ let offset = topLeft + y * width;
+ for (let x = 0; x < dx; ++x) {
+ const black = step(bicubicFiltering(buffer, fx * x, invDeltaY * y) * 160.0);
+ clouds[offset++] = 255 - black;
+ }
+ }
+
+ previousX = currentX;
+ currentX += sliceWidth;
+
+ if (currentX > width) {
+ currentX = width;
+ }
+ }
+ }
+
+ return { clouds, width, height };
+}
+
+function clampIndex(index, size) {
+ return index < 0 ? 0 : index > size - 1 ? size - 1 : index;
+}
+
+function step(x) {
+ return lookup[Math.floor(clampIndex(x, 160))];
+}
+
+function cubicInterpolate(y0, y1, y2, y3, m) {
+ const a0 = -y0 * 0.5 + 3.0 * y1 * 0.5 - 3.0 * y2 * 0.5 + y3 * 0.5;
+ const a1 = y0 - 5.0 * y1 * 0.5 + 2.0 * y2 - y3 * 0.5;
+ const a2 = -y0 * 0.5 + y2 * 0.5;
+ return a0 * m ** 3 + a1 * m ** 2 + a2 * m + y1;
+}
+
+function bicubicFiltering(m, s, t) {
+ return cubicInterpolate(
+ cubicInterpolate(m[0], m[1], m[2], m[3], s),
+ cubicInterpolate(m[4], m[5], m[6], m[7], s),
+ cubicInterpolate(m[8], m[9], m[10], m[11], s),
+ cubicInterpolate(m[12], m[13], m[14], m[15], s),
+ t
+ );
+}
+
+// Draw the clouds on a canvas.
+// This function is useful for debugging.
+function cloudsToCanvas({ clouds, width, height, canvas }) {
+ if (canvas == null) {
+ canvas = document.createElement("canvas");
+ }
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+ let imageData = ctx.getImageData(0, 0, width, height);
+ let imgData = imageData.data;
+
+
+ let srcOffset = 0;
+ let dstOffset = 0;
+ for (let x = 0; x < width; ++x) {
+ for (let y = 0; y < height; ++y) {
+ const color = clouds[srcOffset++];
+ imgData[dstOffset++] = color;
+ imgData[dstOffset++] = color;
+ imgData[dstOffset++] = color;
+ imgData[dstOffset++] = color < 245 ? 255 : 0;
+ }
+ }
+
+
+ ctx.putImageData(imageData, 0, 0);
+ ctx.drawImage(canvas, 0, 0, width, height);
+
+ return canvas;
+}
+
+var clouds = {
+ computeClouds,
+ cloudsToCanvas
+};/* eslint-disable */
+const t={};
+function n(t,n){return t
n?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o;}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1;}return r}}}var r=e(n),i=r.right;var o=Math.sqrt(50),a=Math.sqrt(10),u=Math.sqrt(2);function s(t,n,e){var r,i,o,a,u=-1;if(e=+e,(t=+t)===(n=+n)&&e>0)return [t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(s>=o?10:s>=a?5:s>=u?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(s>=o?10:s>=a?5:s>=u?2:1)}var c=Array.prototype.slice;function h(t){return t}var f=1e-6;function p(t){return "translate("+(t+.5)+",0)"}function d(t){return "translate(0,"+(t+.5)+")"}function g$1(t){return function(n){return +t(n)}}function v(t){var n=Math.max(0,t.bandwidth()-1)/2;return t.round()&&(n=Math.round(n)),function(e){return +t(e)+n}}function m(){return !this.__axis}function y(t,n){var e=[],r=null,i=null,o=6,a=6,u=3,s=1===t||4===t?-1:1,l=4===t||2===t?"x":"y",y=1===t||3===t?p:d;function _(c){var p=null==r?n.ticks?n.ticks.apply(n,e):n.domain():r,d=null==i?n.tickFormat?n.tickFormat.apply(n,e):h:i,_=Math.max(o,0)+u,w=n.range(),x=+w[0]+.5,b=+w[w.length-1]+.5,M=(n.bandwidth?v:g$1)(n.copy()),k=c.selection?c.selection():c,N=k.selectAll(".domain").data([null]),A=k.selectAll(".tick").data(p,n).order(),E=A.exit(),S=A.enter().append("g").attr("class","tick"),T=A.select("line"),P=A.select("text");N=N.merge(N.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),A=A.merge(S),T=T.merge(S.append("line").attr("stroke","currentColor").attr(l+"2",s*o)),P=P.merge(S.append("text").attr("fill","currentColor").attr(l,s*_).attr("dy",1===t?"0em":3===t?"0.71em":"0.32em")),c!==k&&(N=N.transition(c),A=A.transition(c),T=T.transition(c),P=P.transition(c),E=E.transition(c).attr("opacity",f).attr("transform",(function(t){return isFinite(t=M(t))?y(t):this.getAttribute("transform")})),S.attr("opacity",f).attr("transform",(function(t){var n=this.parentNode.__axis;return y(n&&isFinite(n=n(t))?n:M(t))}))),E.remove(),N.attr("d",4===t||2==t?a?"M"+s*a+","+x+"H0.5V"+b+"H"+s*a:"M0.5,"+x+"V"+b:a?"M"+x+","+s*a+"V0.5H"+b+"V"+s*a:"M"+x+",0.5H"+b),A.attr("opacity",1).attr("transform",(function(t){return y(M(t))})),T.attr(l+"2",s*o),P.attr(l,s*_).text(d),k.filter(m).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",2===t?"start":4===t?"end":"middle"),k.each((function(){this.__axis=M;}));}return _.scale=function(t){return arguments.length?(n=t,_):n},_.ticks=function(){return e=c.call(arguments),_},_.tickArguments=function(t){return arguments.length?(e=null==t?[]:c.call(t),_):e.slice()},_.tickValues=function(t){return arguments.length?(r=null==t?null:c.call(t),_):r&&r.slice()},_.tickFormat=function(t){return arguments.length?(i=t,_):i},_.tickSize=function(t){return arguments.length?(o=a=+t,_):o},_.tickSizeInner=function(t){return arguments.length?(o=+t,_):o},_.tickSizeOuter=function(t){return arguments.length?(a=+t,_):a},_.tickPadding=function(t){return arguments.length?(u=+t,_):u},_}var _={value:function(){}};function w(){for(var t,n=0,e=arguments.length,r={};n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return {type:t,name:e}}))}function M(t,n){for(var e,r=0,i=t.length;r0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),A.hasOwnProperty(n)?{space:A[n],local:t}:t}function S(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===N&&n.documentElement.namespaceURI===N?n.createElement(t):n.createElementNS(e,t)}}function T(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function P(t){var n=E(t);return (n.local?T:S)(n)}function C(){}function q(t){return null==t?C:function(){return this.querySelector(t)}}function z(){return []}function L$2(t){return null==t?z:function(){return this.querySelectorAll(t)}}function j(t){return function(){return this.matches(t)}}function X(t){return new Array(t.length)}function O(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n;}O.prototype={constructor:O,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};function V(t,n,e,r,i,o){for(var a,u=0,s=n.length,l=o.length;un?1:t>=n?0:NaN}function D(t){return function(){this.removeAttribute(t);}}function $(t){return function(){this.removeAttributeNS(t.space,t.local);}}function H(t,n){return function(){this.setAttribute(t,n);}}function F(t,n){return function(){this.setAttributeNS(t.space,t.local,n);}}function Y(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e);}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e);}}function U(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function G(t){return function(){this.style.removeProperty(t);}}function Z(t,n,e){return function(){this.style.setProperty(t,n,e);}}function K(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e);}}function Q(t,n){return t.style.getPropertyValue(n)||U(t).getComputedStyle(t,null).getPropertyValue(n)}function J(t){return function(){delete this[t];}}function W(t,n){return function(){this[t]=n;}}function tt(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e;}}function nt(t){return t.trim().split(/^|\s+/)}function et(t){return t.classList||new rt(t)}function rt(t){this._node=t,this._names=nt(t.getAttribute("class")||"");}function it(t,n){for(var e=et(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")));},contains:function(t){return this._names.indexOf(t)>=0}};var xt={},bt=null;"undefined"!=typeof document&&("onmouseenter"in document.documentElement||(xt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Mt(t,n,e){return t=kt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n);}}function kt(t,n,e){return function(r){var i=bt;bt=r;try{t.call(this,this.__data__,n,e);}finally{bt=i;}}}function Nt(t){return t.trim().split(/^|\s+/).map((function(t){var n="",e=t.indexOf(".");return e>=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=w&&(w=_+1);!(y=v[w])&&++w=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=I);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?G:"function"==typeof n?K:Z)(t,n,null==e?"":e)):Q(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?J:"function"==typeof n?tt:W)(t,n)):this.node()[t]},classed:function(t,n){var e=nt(t+"");if(arguments.length<2){for(var r=et(this.node()),i=-1,o=e.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?vn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?vn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=on.exec(t))?new _n(n[1],n[2],n[3],1):(n=an.exec(t))?new _n(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?vn(n[1],n[2],n[3],n[4]):(n=sn.exec(t))?vn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=ln.exec(t))?Mn(n[1],n[2]/100,n[3]/100,1):(n=cn.exec(t))?Mn(n[1],n[2]/100,n[3]/100,n[4]):hn.hasOwnProperty(t)?gn(hn[t]):"transparent"===t?new _n(NaN,NaN,NaN,0):null}function gn(t){return new _n(t>>16&255,t>>8&255,255&t,1)}function vn(t,n,e,r){return r<=0&&(t=n=e=NaN),new _n(t,n,e,r)}function mn(t){return t instanceof Qt||(t=dn(t)),t?new _n((t=t.rgb()).r,t.g,t.b,t.opacity):new _n}function yn(t,n,e,r){return 1===arguments.length?mn(t):new _n(t,n,e,null==r?1:r)}function _n(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r;}function wn(){return "#"+bn(this.r)+bn(this.g)+bn(this.b)}function xn(){var t=this.opacity;return (1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function bn(t){return ((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Mn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Nn(t,n,e,r)}function kn(t){if(t instanceof Nn)return new Nn(t.h,t.s,t.l,t.opacity);if(t instanceof Qt||(t=dn(t)),!t)return new Nn;if(t instanceof Nn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,s=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&s<1?0:a,new Nn(a,u,s,t.opacity)}function Nn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r;}function An(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function En(t){return function(){return t}}function Sn(t){return 1==(t=+t)?Tn:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):En(isNaN(n)?e:n)}}function Tn(t,n){var e=n-t;return e?function(t,n){return function(e){return t+e*n}}(t,e):En(isNaN(t)?n:t)}Zt(Qt,dn,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:fn,formatHex:fn,formatHsl:function(){return kn(this).formatHsl()},formatRgb:pn,toString:pn}),Zt(_n,yn,Kt(Qt,{brighter:function(t){return t=null==t?Wt:Math.pow(Wt,t),new _n(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Jt:Math.pow(Jt,t),new _n(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return -.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:wn,formatHex:wn,formatRgb:xn,toString:xn})),Zt(Nn,(function(t,n,e,r){return 1===arguments.length?kn(t):new Nn(t,n,e,null==r?1:r)}),Kt(Qt,{brighter:function(t){return t=null==t?Wt:Math.pow(Wt,t),new Nn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Jt:Math.pow(Jt,t),new Nn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new _n(An(t>=240?t-240:t+120,i,r),An(t,i,r),An(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return (0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return (1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));var Pn=function t(n){var e=Sn(n);function r(t,n){var r=e((t=yn(t)).r,(n=yn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=Tn(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function Cn(t,n){n||(n=[]);var e,r=t?Math.min(n.length,t.length):0,i=n.slice();return function(o){for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,s.push({i:a,x:Ln(e,r)})),o=On.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:Ln(t,n)})):n&&e.push(i(e)+"rotate("+n+r);}(o.rotate,a.rotate,u,s),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:Ln(t,n)}):n&&e.push(i(e)+"skewX("+n+r);}(o.skewX,a.skewX,u,s),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:Ln(t,e)},{i:u-2,x:Ln(n,r)});}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")");}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,s),o=a=null,function(t){for(var n,e=-1,r=s.length;++e=0&&n._call.call(null,t),n=n._next;--Wn;}();}finally{Wn=0,function(){var t,n,e=Zn,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Zn=n);Kn=t,pe(r);}(),re=0;}}function fe(){var t=oe.now(),n=t-ee;n>1e3&&(ie-=n,ee=t);}function pe(t){Wn||(te&&(te=clearTimeout(te)),t-re>24?(t<1/0&&(te=setTimeout(he,t-oe.now()-ie)),ne&&(ne=clearInterval(ne))):(ne||(ee=oe.now(),ne=setInterval(fe,1e3)),Wn=1,ae(he)));}function de(t,n,e){var r=new le;return n=null==n?0:+n,r.restart((function(e){r.stop(),t(e+n);}),n,e),r}le.prototype=ce.prototype={constructor:le,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?ue():+e)+(null==n?0:+n),this._next||Kn===this||(Kn?Kn._next=this:Zn=this,Kn=this),this._call=t,this._time=e,pe();},stop:function(){this._call&&(this._call=null,this._time=1/0,pe());}};var ge=w("start","end","cancel","interrupt"),ve=[];function me(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=1,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay);}function a(o){var l,c,h,f;if(1!==e.state)return s();for(l in i)if((f=i[l]).name===e.name){if(3===f.state)return de(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[l]):+l0)throw new Error("too late; already scheduled");return e}function _e(t,n){var e=we(t,n);if(e.state>3)throw new Error("too late; already running");return e}function we(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function xe(t,n){var e,r;return function(){var i=_e(this,t),o=i.tween;if(o!==e)for(var a=0,u=(r=e=o).length;a=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ye:_e;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i;}}var De=Lt.prototype.constructor;function $e(t){return function(){this.style.removeProperty(t);}}function He(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e);}}function Fe(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&He(t,o,e)),r}return o._value=n,o}function Ye(t){return function(n){this.textContent=t.call(this,n);}}function Be(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&Ye(r)),n}return r._value=t,r}var Ue=0;function Ge(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r;}function Ze(){return ++Ue}var Ke=Lt.prototype;Ge.prototype={constructor:Ge,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=q(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a2&&e.state<5,e.state=6,e.timer.stop(),e.on.call(r?"interrupt":"cancel",t,t.__data__,e.index,e.group),delete o[i]):a=!1;a&&delete t.__transition;}}(this,t);}))},Lt.prototype.transition=function(t){var n,e;t instanceof Ge?(n=t._id,t=t._name):(n=Ze(),(e=Qe).time=ue(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,o=0;onr)if(Math.abs(c*u-s*l)>nr&&i){var f=e-o,p=r-a,d=u*u+s*s,g=f*f+p*p,v=Math.sqrt(d),m=Math.sqrt(h),y=i*Math.tan((We-Math.acos((d+h-g)/(2*v*m)))/2),_=y/m,w=y/v;Math.abs(_-1)>nr&&(this._+="L"+(t+_*l)+","+(n+_*c)),this._+="A"+i+","+i+",0,0,"+ +(c*f>l*p)+","+(this._x1=t+w*u)+","+(this._y1=n+w*s);}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,o=!!o;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),s=t+a,l=n+u,c=1^o,h=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+l:(Math.abs(this._x1-s)>nr||Math.abs(this._y1-l)>nr)&&(this._+="L"+s+","+l),e&&(h<0&&(h=h%tr+tr),h>er?this._+="A"+e+","+e+",0,1,"+c+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=l):h>nr&&(this._+="A"+e+","+e+",0,"+ +(h>=We)+","+c+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))));},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z";},toString:function(){return this._}};var or="$";function ar(){}function ur(t,n){var e=new ar;if(t instanceof ar)t.each((function(t,n){e.set(n,t);}));else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++i1?r[0]+r.slice(2):r,+t.slice(e+1)]}function hr(t){return (t=cr(Math.abs(t)))?t[1]:NaN}sr.prototype={constructor:sr,has:lr.has,add:function(t){return this[or+(t+="")]=t,this},remove:lr.remove,clear:lr.clear,values:lr.keys,size:lr.size,empty:lr.empty,each:lr.each};var fr,pr=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function dr(t){if(!(n=pr.exec(t)))throw new Error("invalid format: "+t);var n;return new gr({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function gr(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+"";}function vr(t,n){var e=cr(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}dr.prototype=gr.prototype,gr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var mr={"%":function(t,n){return (100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return vr(100*t,n)},r:vr,s:function(t,n){var e=cr(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fr=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+cr(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function yr(t){return t}var _r,wr,xr=Array.prototype.map,br=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Mr(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?yr:(n=xr.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],s=0;i>0&&u>0&&(s+u+1>r&&(u=Math.max(1,r-s)),o.push(t.substring(i-=u,i+u)),!((s+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?yr:function(t){return function(n){return n.replace(/[0-9]/g,(function(n){return t[+n]}))}}(xr.call(t.numerals,String)),s=void 0===t.percent?"%":t.percent+"",l=void 0===t.minus?"-":t.minus+"",c=void 0===t.nan?"NaN":t.nan+"";function h(t){var n=(t=dr(t)).fill,e=t.align,h=t.sign,f=t.symbol,p=t.zero,d=t.width,g=t.comma,v=t.precision,m=t.trim,y=t.type;"n"===y?(g=!0,y="g"):mr[y]||(void 0===v&&(v=12),m=!0,y="g"),(p||"0"===n&&"="===e)&&(p=!0,n="0",e="=");var _="$"===f?i:"#"===f&&/[boxX]/.test(y)?"0"+y.toLowerCase():"",w="$"===f?o:/[%p]/.test(y)?s:"",x=mr[y],b=/[defgprs%]/.test(y);function M(t){var i,o,s,f=_,M=w;if("c"===y)M=x(t)+M,t="";else {var k=(t=+t)<0||1/t<0;if(t=isNaN(t)?c:x(Math.abs(t),v),m&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0);}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),k&&0==+t&&"+"!==h&&(k=!1),f=(k?"("===h?h:l:"-"===h||"("===h?"":h)+f,M=("s"===y?br[8+fr/3]:"")+M+(k&&"("===h?")":""),b)for(i=-1,o=t.length;++i(s=t.charCodeAt(i))||s>57){M=(46===s?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}g&&!p&&(t=r(t,1/0));var N=f.length+t.length+M.length,A=N>1)+f+t+M+A.slice(N);break;default:t=A+f+t+M;}return u(t)}return v=void 0===v?6:/[gprs]/.test(y)?Math.max(1,Math.min(21,v)):Math.max(0,Math.min(20,v)),M.toString=function(){return t+""},M}return {format:h,formatPrefix:function(t,n){var e=h(((t=dr(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(hr(n)/3))),i=Math.pow(10,-r),o=br[8+r/3];return function(t){return e(i*t)+o}}}}function kr(t,n){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(n).domain(t);}return this}t.format=void 0,_r=Mr({decimal:".",thousands:",",grouping:[3],currency:["$",""],minus:"-"}),t.format=_r.format,wr=_r.formatPrefix;var Nr=Array.prototype,Ar=Nr.map,Er=Nr.slice;function Sr(t){return +t}var Tr=[0,1];function Pr(t){return t}function Cr(t,n){return (n-=t=+t)?function(e){return (e-t)/n}:function(t){return function(){return t}}(isNaN(n)?NaN:.5)}function qr(t){var n,e=t[0],r=t[t.length-1];return e>r&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function zr(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?Lr:zr,i=o=null,h}function h(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,s)))(t(l(n)))}return h.invert=function(e){return l(n((o||(o=r(u,a.map(t),Ln)))(e)))},h.domain=function(t){return arguments.length?(a=Ar.call(t,Sr),l===Pr||(l=qr(a)),c()):a.slice()},h.range=function(t){return arguments.length?(u=Er.call(t),c()):u.slice()},h.rangeRound=function(t){return u=Er.call(t),s=In,c()},h.clamp=function(t){return arguments.length?(l=t?qr(a):Pr,h):l!==Pr},h.interpolate=function(t){return arguments.length?(s=t,c()):s},h.unknown=function(t){return arguments.length?(e=t,h):e},function(e,r){return t=e,n=r,c()}}function Or(t,n){return Xr()(t,n)}function Vr(n,e,r,i){var s,l=function(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),s=r/i;return s>=o?i*=10:s>=a?i*=5:s>=u&&(i*=2),n0?r=l(u=Math.floor(u/r)*r,s=Math.ceil(s/r)*r,e):r<0&&(r=l(u=Math.ceil(u*r)/r,s=Math.floor(s*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(s/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(s*r)/r,n(i)),t},t}function Ir(t){return Math.log(t)}function Dr(t){return Math.exp(t)}function $r(t){return -Math.log(-t)}function Hr(t){return -Math.exp(-t)}function Fr(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Yr(t){return function(n){return -t(-n)}}function Br(n){var e,r,i=n(Ir,Dr),o=i.domain,a=10;function u(){return e=function(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}(a),r=function(t){return 10===t?Fr:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}(a),o()[0]<0?(e=Yr(e),r=Yr(r),n($r,Hr)):n(Ir,Dr),i}return i.base=function(t){return arguments.length?(a=+t,u()):a},i.domain=function(t){return arguments.length?(o(t),u()):o()},i.ticks=function(t){var n,i=o(),u=i[0],l=i[i.length-1];(n=l0){for(;pl)break;v.push(f);}}else for(;p=1;--h)if(!((f=c*h)l)break;v.push(f);}}else v=s(p,d,Math.min(d-p,g)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*ah;}s.mouse("drag");}function g(){jt(bt.view).on("mousemove.drag mouseup.drag",null),function(t,n){var e=t.document.documentElement,r=jt(t).on("dragstart.drag",null);n&&(r.on("click.drag",Dt,!0),setTimeout((function(){r.on("click.drag",null);}),0)),"onselectstart"in e?r.on("selectstart.drag",null):(e.style.MozUserSelect=e.__noselect,delete e.__noselect);}(bt.view,e),Dt(),s.mouse("end");}function v(){if(i.apply(this,arguments)){var t,n,e=bt.changedTouches,r=o.apply(this,arguments),a=e.length;for(t=0;t plines[plines.length - 1]; i -= tickInterval) pticks.push(i);
+
+ const altticks = [];
+ for (let i = 0; i < 20000; i += (10000 / 3.28084)) altticks.push(atm.pressureFromElevation(i));
+ //console.log(altticks);
+
+ const barbsize = 15; /////
+ // functions for Scales and axes. Note the inverted domain for the y-scale: bigger is up!
+ const r = t.scaleLinear().range([0, 300]).domain([0, 150]);
+ t.scaleLinear();
+ const bisectTemp = t.bisector(function (d) { return d.press; }).left; // bisector function for tooltips
+ let w, h, x, y, xAxis, yAxis, yAxis2, yAxis3;
+ let ymax; //log scale for max top pressure
+
+ let dataReversed = [];
+ let dataAr = [];
+ //aux
+ const unitSpd = "kt"; // or kmh
+ let unitAlt = "m";
+ let windDisplay = "Barbs";
+
+ if (isTouchDevice === void 0) {
+ if (L && L.version) { //check if leaflet is loaded globally
+ if (L.Browser.mobile) isTouchDevice = true;
+ } else {
+ isTouchDevice = ('ontouchstart' in window) ||
+ (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
+ }
+ }
+ //console.log("this is a touch device:", isTouchDevice);
+
+
+
+ //containers
+ const wrapper = outerWrapper.append("div").style("position","relative");
+ const cloudContainer = wrapper.append("div").attr("class", "cloud-container");
+ const svg = wrapper.append("svg").attr("class", "mainsvg"); //main svg
+ const controls = wrapper.append("div").attr("class", "controls fnt controls1");
+ const valuesContainer = wrapper.append("div").attr("class", "controls fnt controls2");
+ const rangeContainer = wrapper.append("div").attr("class", "range-container fnt");
+ const rangeContainer2 = wrapper.append("div").attr("class", "range-container-extra fnt");
+ const container = svg.append("g");//.attr("id", "container"); //container
+ const skewtbg = container.append("g").attr("class", "skewtbg");//.attr("id", "skewtbg");//background
+ const skewtgroup = container.append("g").attr("class", "skewt"); // put skewt lines in this group (class skewt not used)
+ const barbgroup = container.append("g").attr("class", "windbarb"); // put barbs in this group
+ const tooltipgroup = container.append("g").attr("class", "tooltips"); //class tooltps not used
+ const tooltipRect = container.append("rect").attr("class", "overlay");//.attr("id", "tooltipRect")
+ const cloudCanvas1 = cloudContainer.append("canvas").attr("width", 1).attr("height", 200).attr("class", "cloud"); //original = width 10 and height 300
+ this.cloudRef1 = cloudCanvas1.node();
+ const cloudCanvas2 = cloudContainer.append("canvas").attr("width", 1).attr("height", 200).attr("class", "cloud");
+ this.cloudRef2 = cloudCanvas2.node();
+
+
+ function getFlags(f) {
+ const flags = {
+ "131072": "surface",
+ "65536": "standard level",
+ "32768": "tropopause level",
+ "16384": "maximum wind level",
+ "8192": "significant temperature level",
+ "4096": "significant humidity level",
+ "2048": "significant wind level",
+ "1024": "beginning of missing temperature data",
+ "512": "end of missing temperature data",
+ "256": "beginning of missing humidity data",
+ "128": "end of missing humidity data",
+ "64": "beginning of missing wind data",
+ "32": "end of missing wind data",
+ "16": "top of wind sounding",
+ "8": "level determined by regional decision",
+ "4": "reserved",
+ "2": "pressure level vertical coordinate"
+ };
+
+ const foundflags = [];
+ const decode = (a, i) => {
+ if (a % 2) foundflags.push(flags[1 << i]);
+ if (a) decode(a >> 1, i + 1);
+ };
+ decode(f, 0);
+ //console.log(foundflags);
+ return foundflags;
+ }
+
+
+
+ //local functions
+ function setVariables() {
+ width = parseInt(wrapper.style('width'), 10);
+ height = height || width;
+ //if (height>width) height = width;
+ w = width - margin.left - margin.right;
+ h = height - margin.top - margin.bottom;
+ tan = Math.tan((gradient || 55) * deg2rad);
+ //use the h for the x range, so that appearance does not change when resizing, remains square
+
+ ymax = t.scaleLog().range([0 ,h ]).domain([maxtopp, basep]);
+ y = t.scaleLog().range([0 ,h ]).domain([topp, basep]);
+
+ temprange = init_temprange * (h-ymax(topp))/ (h-ymax(maxtopp));
+ x = t.scaleLinear().range([w/2 - h*2, w/2 + h*2]).domain([midtemp - temprange * 4, midtemp + temprange * 4]); //range is w*2
+
+ xAxisTicks = temprange < 40 ? 30: 40;
+ xAxis = t.axisBottom(x).tickSize(0, 0).ticks(xAxisTicks);//.orient("bottom");
+ yAxis = t.axisLeft(y).tickSize(0, 0).tickValues(plines.filter(p => (p % 100 == 0 || p == 50 || p == 150))).tickFormat(t.format(".0d"));//.orient("left");
+ yAxis2 = t.axisRight(y).tickSize(5, 0).tickValues(pticks);//.orient("right");
+ yAxis3 = t.axisLeft(y).tickSize(2, 0).tickValues(altticks);
+
+ steph = atm.getElevation(topp) / (h/12);
+
+ }
+
+ function convSpd(msvalue, unit) {
+ switch (unit) {
+ case "kt":
+ return msvalue * 1.943844492;
+ case "kmh":
+ return msvalue * 3.6;
+ default:
+ return msvalue;
+ }
+ }
+ function convAlt(v, unit) {
+ switch (unit) {
+ case "m":
+ return Math.round(v) + unit;
+ case "f":
+ return Math.round(v * 3.28084) + "ft";
+ default:
+ return v;
+ }
+ }
+
+ //assigns d3 events
+ t.select(window).on('resize', resize);
+
+ function resize() {
+ skewtbg.selectAll("*").remove();
+ setVariables();
+ svg.attr("width", w + margin.right + margin.left).attr("height", h + margin.top + margin.bottom);
+ container.attr("transform", "translate(" + margin.left + "," + (margin.top) + ")");
+ drawBackground();
+ dataAr.forEach(d => {
+ plot(d.data, { add: true, select: false });
+ });//redraw each plot
+ if (selectedSkewt) selectSkewt(selectedSkewt.data);
+ shiftXAxis();
+ tooltipRect.attr("width", w).attr("height", h);
+
+ cloudContainer.style("left", (margin.left + 2) + "px").style("top", margin.top + "px").style("height", h + "px");
+ const canTop = y(100); //top of canvas for pressure 100
+ cloudCanvas1.style("left", "0px").style("top", canTop + "px").style("height", (h - canTop) + "px");
+ cloudCanvas2.style("left", "10px").style("top", canTop + "px").style("height", (h - canTop) + "px");
+ }
+
+ const lines = {};
+ let clipper;
+ let xAxisValues;
+ //let tempLine, tempdewLine; now in object
+
+
+ const drawBackground = function () {
+
+ // Add clipping path
+ clipper = skewtbg.append("clipPath")
+ .attr("id", "clipper")
+ .append("rect")
+ .attr("x", 0)
+ .attr("y", 0 )
+ .attr("width", w)
+ .attr("height", h );
+
+ // Skewed temperature lines
+ lines.temp = skewtbg.selectAll("templine")
+ .data(t.scaleLinear().domain([midtemp - temprange * 4, midtemp + temprange*4]).ticks(xAxisTicks))
+ .enter().append("line")
+ .attr("x1", d => x(d) - 0.5 + (y(basep) - y(topp)) / tan)
+ .attr("x2", d => x(d) - 0.5)
+ .attr("y1", 0)
+ .attr("y2", h)
+ .attr("class", d => d == 0 ? `tempzero ${buttons["Temp"].hi ? "highlight-line" : ""}` : `templine ${buttons["Temp"].hi ? "highlight-line" : ""}`)
+ .attr("clip-path", "url(#clipper)");
+ //.attr("transform", "translate(0," + h + ") skewX(-30)");
+
+
+ /*
+ let topTempOffset = x.invert(h/tan + w/2);
+ let elevDiff = (atm.getElevation(topp) - atm.getElevation(basep));// * 3.28084;
+ let km11y = h*(11000 - atm.getElevation(basep)) / elevDiff;
+ let tempOffset11 = x.invert(km11y/tan + w/2);
+
+ console.log("top temp shift", tempOffset11, x.invert(km11y/tan) ) ;//(elevDiff/304.8)); //deg per 1000ft
+ */
+
+ const pp = moving ?
+ [basep, basep - (basep - topp) * 0.25, basep - (basep - topp) * 0.5, basep - (basep - topp) * 0.75, topp]
+ : t.range(basep, topp - 50, pIncrement);
+
+
+ const pAt11km = atm.pressureFromElevation(11000);
+ //console.log(pAt11km);
+
+ const elrFx = t.line()
+ .curve(t.curveLinear)
+ .x(function (d, i) {
+ atm.getElevation2(d);
+ const t = d > pAt11km ? 15 - atm.getElevation(d) * 0.00649 : -56.5; //6.49 deg per 1000 m
+ return x(t) + (y(basep) - y(d)) / tan;
+ })
+ .y(function (d, i) { return y(d) });
+
+ lines.elr = skewtbg.selectAll("elr")
+ .data([plines.filter(p => p > pAt11km).concat([pAt11km, 50])])
+ .enter().append("path")
+ .attr("d", elrFx)
+ .attr("clip-path", "url(#clipper)")
+ .attr("class", `elr ${showElr ? "highlight-line" : ""}`);
+
+ // Logarithmic pressure lines
+ lines.pressure = skewtbg.selectAll("pressureline")
+ .data(plines)
+ .enter().append("line")
+ .attr("x1", - w)
+ .attr("x2", 2 * w)
+ .attr("y1", y)
+ .attr("y2", y)
+ .attr("clip-path", "url(#clipper)")
+ .attr("class", `pressure ${buttons["Pressure"].hi ? "highlight-line" : ""}`);
+
+ // create array to plot adiabats
+
+ const dryad = t.scaleLinear().domain([midtemp - temprange * 2, midtemp + temprange * 6]).ticks(xAxisTicks);
+
+ const all = [];
+
+ for (let i = 0; i < dryad.length; i++) {
+ const z = [];
+ for (let j = 0; j < pp.length; j++) { z.push(dryad[i]); }
+ all.push(z);
+ }
+
+
+ const drylineFx = t.line()
+ .curve(t.curveLinear)
+ .x(function (d, i) {
+ return x(
+ atm.dryLapse(pp[i], K0 + d, basep) - K0
+ ) + (y(basep) - y(pp[i])) / tan;
+ })
+ .y(function (d, i) { return y(pp[i]) });
+
+ // Draw dry adiabats
+ lines.dryadiabat = skewtbg.selectAll("dryadiabatline")
+ .data(all)
+ .enter().append("path")
+ .attr("class", `dryadiabat ${buttons["Dry Adiabat"].hi ? "highlight-line" : ""}`)
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", drylineFx);
+
+ // moist adiabat fx
+ let temp;
+ const moistlineFx = t.line()
+ .curve(t.curveLinear)
+ .x(function (d, i) {
+ temp = i == 0 ? K0 + d : ((temp + atm.moistGradientT(pp[i], temp) * (moving ? (topp - basep) / 4 : pIncrement)));
+ return x(temp - K0) + (y(basep) - y(pp[i])) / tan;
+ })
+ .y(function (d, i) { return y(pp[i]) });
+
+ // Draw moist adiabats
+ lines.moistadiabat = skewtbg.selectAll("moistadiabatline")
+ .data(all)
+ .enter().append("path")
+ .attr("class", `moistadiabat ${buttons["Moist Adiabat"].hi ? "highlight-line" : ""}`)
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", moistlineFx);
+
+ // isohume fx
+ let mixingRatio;
+ const isohumeFx = t.line()
+ .curve(t.curveLinear)
+ .x(function (d, i) {
+ //console.log(d);
+ if (i == 0) mixingRatio = atm.mixingRatio(atm.saturationVaporPressure(d + K0), pp[i]);
+ temp = atm.dewpoint(atm.vaporPressure(pp[i], mixingRatio));
+ return x(temp - K0) + (y(basep) - y(pp[i])) / tan;
+ })
+ .y(function (d, i) { return y(pp[i]) });
+
+ // Draw isohumes
+ lines.isohume = skewtbg.selectAll("isohumeline")
+ .data(all)
+ .enter().append("path")
+ .attr("class", `isohume ${buttons["Isohume"].hi ? "highlight-line" : ""}`)
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", isohumeFx);
+
+ // Line along right edge of plot
+ skewtbg.append("line")
+ .attr("x1", w - 0.5)
+ .attr("x2", w - 0.5)
+ .attr("y1", 0)
+ .attr("y2", h)
+ .attr("class", "gridline");
+
+ // Add axes
+ xAxisValues = skewtbg.append("g").attr("class", "x axis").attr("transform", "translate(0," + (h - 0.5 ) + ")").call(xAxis).attr("clip-path", "url(#clipper)");
+ skewtbg.append("g").attr("class", "y axis").attr("transform", "translate(-0.5,0)").call(yAxis);
+ skewtbg.append("g").attr("class", "y axis ticks").attr("transform", "translate(-0.5,0)").call(yAxis2);
+ skewtbg.append("g").attr("class", "y axis hght-ticks").attr("transform", "translate(-0.5,0)").call(yAxis3);
+ };
+
+ const makeBarbTemplates = function () {
+ const speeds = t.range(5, 205, 5);
+ const barbdef = container.append('defs');
+ speeds.forEach(function (d) {
+ const thisbarb = barbdef.append('g').attr('id', 'barb' + d);
+ const flags = Math.floor(d / 50);
+ const pennants = Math.floor((d - flags * 50) / 10);
+ const halfpennants = Math.floor((d - flags * 50 - pennants * 10) / 5);
+ let px = barbsize / 2;
+ // Draw wind barb stems
+ thisbarb.append("line").attr("x1", 0).attr("x2", 0).attr("y1", -barbsize / 2).attr("y2", barbsize / 2);
+ // Draw wind barb flags and pennants for each stem
+ for (var i = 0; i < flags; i++) {
+ thisbarb.append("polyline")
+ .attr("points", "0," + px + " -6," + (px) + " 0," + (px - 2))
+ .attr("class", "flag");
+ px -= 5;
+ }
+ // Draw pennants on each barb
+ for (i = 0; i < pennants; i++) {
+ thisbarb.append("line")
+ .attr("x1", 0)
+ .attr("x2", -6)
+ .attr("y1", px)
+ .attr("y2", px + 2);
+ px -= 3;
+ }
+ // Draw half-pennants on each barb
+ for (i = 0; i < halfpennants; i++) {
+ thisbarb.append("line")
+ .attr("x1", 0)
+ .attr("x2", -3)
+ .attr("y1", px)
+ .attr("y2", px + 1);
+ px -= 3;
+ }
+ });
+ };
+
+
+ const shiftXAxis = function () {
+ clipper.attr("x", -xOffset);
+ xAxisValues.attr("transform", `translate(${xOffset}, ${h - 0.5} )`);
+ for (const p in lines) {
+ lines[p].attr("transform", `translate(${xOffset},0)`);
+ } dataAr.forEach(d => {
+ for (const p in d.lines) {
+ d.lines[p].attr("transform", `translate(${xOffset},0)`);
+ }
+ });
+ };
+
+
+ const drawToolTips = function () {
+
+ // Draw tooltips
+ const tmpcfocus = tooltipgroup.append("g").attr("class", "focus tmpc");
+ tmpcfocus.append("circle").attr("r", 4);
+ tmpcfocus.append("text").attr("x", 9).attr("dy", ".35em");
+
+ const dwpcfocus = tooltipgroup.append("g").attr("class", "focus dwpc");
+ dwpcfocus.append("circle").attr("r", 4);
+ dwpcfocus.append("text").attr("x", -9).attr("text-anchor", "end").attr("dy", ".35em");
+
+ const hghtfocus = tooltipgroup.append("g").attr("class", "focus");
+ const hght1 = hghtfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", ".35em");
+ const hght2 = hghtfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", "-0.65em").style("fill", "blue");
+
+ const wspdfocus = tooltipgroup.append("g").attr("class", "focus windspeed");
+ const wspd1 = wspdfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", ".35em");
+ const wspd2 = wspdfocus.append("text").attr("x", "0.8em").attr("text-anchor", "start").attr("dy", "-0.65em").style("fill", "red");
+ const wspd3 = wspdfocus.append("text").attr("class", "skewt-wind-arrow").html("⇩");
+ const wspd4 = wspdfocus.append("text").attr("y", "1em").attr("text-anchor", "start").style("fill", "rgba(0,0,0,0.3)").style("font-size", "10px");
+ //console.log(wspdfocus)
+
+ let startX = null;
+
+
+ function start(e) {
+ showTooltips();
+ move.call(tooltipRect.node());
+ startX = t.mouse(this)[0] - xOffset;
+ }
+
+ function end(e) {
+ startX = null;
+ }
+
+ const hideTooltips = () => {
+ [tmpcfocus, dwpcfocus, hghtfocus, wspdfocus].forEach(e => e.style("display", "none"));
+ currentY = null;
+ };
+ hideTooltips();
+
+ const showTooltips = () => {
+ [tmpcfocus, dwpcfocus, hghtfocus, wspdfocus].forEach(e => e.style("display", null));
+ };
+
+ const move2P = (y0) => {
+ //console.log("mving to", y0);
+ if (y0 || y0===0) showTooltips();
+ const i = bisectTemp(dataReversed, y0, 1, dataReversed.length - 1);
+ const d0 = dataReversed[i - 1];
+ const d1 = dataReversed[i];
+ const d = y0 - d0.press > d1.press - y0 ? d1 : d0;
+ currentY = y0;
+
+ tmpcfocus.attr("transform", "translate(" + (xOffset + x(d.temp) + (y(basep) - y(d.press)) / tan) + "," + y(d.press) + ")");
+ dwpcfocus.attr("transform", "translate(" + (xOffset + x(d.dwpt) + (y(basep) - y(d.press)) / tan) + "," + y(d.press) + ")");
+
+ hghtfocus.attr("transform", "translate(0," + y(d.press) + ")");
+ hght1.html(" " + ((d.hght || d.hght===0) ? convAlt(d.hght, unitAlt):"") ); //hgt or hghtagl ???
+ hght2.html(" " + Math.round(d.dwpt) + "°C");
+
+ wspdfocus.attr("transform", "translate(" + (w - (windDisplay=="Barbs" ? 70:80)) + "," + y(d.press) + ")");
+ wspd1.html(isNaN(d.wspd) ? "" : (Math.round(convSpd(d.wspd, unitSpd) * 10) / 10 + unitSpd));
+ wspd2.html(Math.round(d.temp) + "°C");
+ wspd3.style("transform", `rotate(${d.wdir}deg)`);
+ wspd4.html(d.flags ? getFlags(d.flags).map(f => `${f}`).join() : "");
+ //console.log( getFlags(d.flags).join("
"));
+
+ if (pressCbfs) pressCbfs.forEach(cbf=>cbf(d.press));
+ };
+
+ function move(e) {
+ const newX = t.mouse(this)[0];
+ if (startX !== null) {
+ xOffset = -(startX - newX);
+ shiftXAxis();
+ }
+ const y0 = y.invert(t.mouse(this)[1]); // get y value of mouse pointer in pressure space
+ move2P(y0);
+ }
+
+ tooltipRect
+ .attr("width", w)
+ .attr("height", h);
+
+ //.on("mouseover", start)
+ //.on("mouseout", end)
+ //.on("mousemove", move)
+ if (!isTouchDevice) {
+
+ tooltipRect.call(t.drag().on("start", start).on("drag", move).on("end", end));
+ } else {
+ tooltipRect
+ //tooltipRect.node().addEventListener('touchstart',start, true)
+ //tooltipRect.node().addEventListener('touchmove',move, true)
+ //tooltipRect.node().addEventListener('touchend',end, true)
+ .on('touchstart', start)
+ .on('touchmove', move)
+ .on('touchend', end);
+ }
+
+ Object.assign(this, { move2P, hideTooltips, showTooltips });
+ };
+
+
+
+ const drawParcelTraj = function (dataObj) {
+
+ const { data, parctemp } = dataObj;
+
+ if (data[0].dwpt == undefined) return;
+
+ const pt = atm.parcelTrajectory(
+ { level: data.map(e => e.press), gh: data.map(e => e.hght), temp: data.map(e => e.temp + K0) },
+ moving ? 10 : xAxisTicks,
+ parctemp + K0,
+ data[0].press,
+ data[0].dwpt + K0
+ );
+
+ //draw lines
+ const parctrajFx = t.line()
+ .curve(t.curveLinear)
+ .x(function (d, i) { return x(d.t) + (y(basep) - y(d.p)) / tan; })
+ .y(function (d, i) { return y(d.p); });
+
+ //let parcLines={dry:[], moist:[], isohumeToDry:[], isohumeToTemp:[], moistFromCCL:[], TCONline:[], thrm:[], cloud:[]};
+
+ const parcLines = { parcel: [], LCL: [], CCL: [], TCON: [], "THRM top": [], "CLD top": [] };
+
+ for (const prop in parcLines) {
+ const p = prop;
+ if (dataObj.lines[p]) dataObj.lines[p].remove();
+
+ let line = [], press;
+ switch (p) {
+ case "parcel":
+ if (pt.dry) line.push(pt.dry);
+ if (pt.moist) line.push(pt.moist);
+ break;
+ case "TCON":
+ const t = pt.TCON;
+ line = t !== void 0 ? [[[t, basep], [t, topp]]] : [];
+ break;
+ case "LCL":
+ if (pt.isohumeToDry) line.push(pt.isohumeToDry);
+ break;
+ case "CCL":
+ if (pt.isohumeToTemp) line.push(pt.isohumeToTemp);
+ if (pt.moistFromCCL) line.push(pt.moistFromCCL);
+ break;
+ case "THRM top":
+ press = pt.pThermalTop;
+ if (press) line = [[[0, press], [400, press]]];
+ break;
+ case "CLD top":
+ press = pt.pCloudTop;
+ if (press) line = [[[0, press], [400, press]]];
+ break;
+ }
+
+ if (line) parcLines[p] = line.map(e => e.map(ee => { return { t: ee[0] - K0, p: ee[1] } }));
+
+ dataObj.lines[p] = skewtgroup
+ .selectAll(p)
+ .data(parcLines[p]).enter().append("path")
+ .attr("class", `${p == "parcel" ? "parcel" : "cond-level"} ${selectedSkewt && data == selectedSkewt.data && (p == "parcel" || values[p].hi) ? "highlight-line" : ""}`)
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", parctrajFx)
+ .attr("transform", `translate(${xOffset},0)`);
+ }
+
+ //update values
+ for (const p in values) {
+ let v = pt[p == "CLD top" ? "cloudTop" : p == "THRM top" ? "elevThermalTop" : p];
+ let CLDtopHi;
+ if (p == "CLD top" && v == 100000) {
+ v = data[data.length - 1].hght;
+ CLDtopHi = true;
+ }
+ const txt = `${(p[0].toUpperCase() + p.slice(1)).replace(" ", " ")}:
${!v ? "" : p == "TCON" ? (v - K0).toFixed(1) + "°C" : (CLDtopHi ? "> " : "") + convAlt(v, unitAlt)}`;
+ values[p].val.html(txt);
+ }
+ };
+
+ const selectSkewt = function (data) { //use the data, then can be found from the outside by using data obj ref
+ dataAr.forEach(d => {
+ const found = d.data == data;
+ for (const p in d.lines) {
+ d.lines[p].classed("highlight-line", found && (!values[p] || values[p].hi));
+ }
+ if (found) {
+ selectedSkewt = d;
+ dataReversed = [].concat(d.data).reverse();
+ ranges.parctemp.input.node().value = ranges.parctemp.value = d.parctemp = Math.round(d.parctemp * 10) / 10;
+ ranges.parctemp.valueDiv.html(html4range(d.parctemp, "parctemp"));
+ }
+ });
+ _this.hideTooltips();
+ };
+
+
+
+ //if in options: add, add new plot,
+ //if select, set selected ix and highlight. if select false, must hightlight separtely.
+ //ixShift used to shift to the right, used when you want to keep position 0 open.
+ //max is the max number of plots, by default at the moment 2,
+ const plot = function (s, { add, select, ixShift = 0, max = 2 } = {}) {
+
+ if (s.length == 0) return;
+
+ let ix = 0; //index of the plot, there may be more than one, to shift barbs and make clouds on canvas
+
+ if (!add) {
+ dataAr.forEach(d => { //clear all plots
+ for (const p in d.lines) d.lines[p].remove();
+ });
+ dataAr = [];
+ [1, 2].forEach(c => {
+ const ctx = _this["cloudRef" + c].getContext("2d");
+ ctx.clearRect(0, 0, 10, 200);
+ });
+ }
+
+ let dataObj = dataAr.find(d => d.data == s);
+
+ let data;
+
+ if (!dataObj) {
+ const parctemp = Math.round((s[0].temp + ranges.parctempShift.value) * 10) / 10;
+ data = s; //do not filter here, filter creates new obj, looses ref
+ //however, object itself can be changed.
+ for(let i = 0; i=0 && data[i].rh<=100)){
+ let {rh, temp} = data[i];
+ data[i].dwpt = 243.04*(Math.log(rh/100)+((17.625*temp)/(243.04+temp)))/(17.625-Math.log(rh/100)-((17.625*temp)/(243.04+temp)));
+ }
+ }
+ ix = dataAr.push({ data, parctemp, lines: {} }) - 1;
+ dataObj = dataAr[ix];
+ if (ix >= max) {
+ console.log("more than max plots added");
+ ix--;
+ setTimeout((ix) => {
+ if (dataAr.length > max) _this.removePlot(dataAr[ix].data);
+ }, 1000, ix);
+ }
+ } else {
+ ix = dataAr.indexOf(dataObj);
+ data = dataObj.data;
+ for (const p in dataObj.lines) dataObj.lines[p].remove();
+ }
+
+ //reset parctemp range if this is the selected range
+ if (select) {
+ ranges.parctemp.input.node().value = ranges.parctemp.value = dataObj.parctemp;
+ ranges.parctemp.valueDiv.html(html4range(dataObj.parctemp, "parctemp"));
+ }
+
+ //skew-t stuff
+
+ // Filter data, depending on range moving, or nullish values
+
+ let data4moving;
+ if (data.length > 50 && moving) {
+ let prev = -1;
+ data4moving = data.filter((e, i, a) => {
+ const n = Math.floor(i * 50 / (a.length - 1));
+ if (n > prev) {
+ prev = n;
+ return true;
+ }
+ });
+ } else {
+ data4moving = data.map(e=>e);
+ }
+ let data4temp = [data4moving.filter(e=>( e.temp || e.temp===0 ) && e.temp>-999 )];
+ let data4dwpt = [data4moving.filter(e=>( e.dwpt || e.dwpt===0 ) && e.dwpt>-999 )];
+
+
+
+
+
+ const templineFx = t.line().curve(t.curveLinear).x(function (d, i) { return x(d.temp) + (y(basep) - y(d.press)) / tan; }).y(function (d, i) { return y(d.press); });
+ dataObj.lines.tempLine = skewtgroup
+ .selectAll("templines")
+ .data(data4temp).enter().append("path")
+ .attr("class", "temp")//(d,i)=> `temp ${i<10?"skline":"mean"}` )
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", templineFx);
+
+ const tempdewlineFx = t.line().curve(t.curveLinear).x(function (d, i) { return x(d.dwpt) + (y(basep) - y(d.press)) / tan; }).y(function (d, i) { return y(d.press); });
+ dataObj.lines.tempdewLine = skewtgroup
+ .selectAll("tempdewlines")
+ .data(data4dwpt).enter().append("path")
+ .attr("class", "dwpt")//(d,i)=>`dwpt ${i<10?"skline":"mean"}` )
+ .attr("clip-path", "url(#clipper)")
+ .attr("d", tempdewlineFx);
+
+ drawParcelTraj(dataObj);
+
+
+
+ const siglines = data
+ .filter((d, i, a, f) => d.flags && (f = getFlags(d.flags), f.includes("tropopause level") || f.includes("surface")) ? d.press : false)
+ .map((d, i, a, f) => (f = getFlags(d.flags), { press: d.press, classes: f.map(e => e.replace(/ /g, "-")).join(" ") }));
+
+ dataObj.lines.siglines = skewtbg.selectAll("siglines")
+ .data(siglines)
+ .enter().append("line")
+ .attr("x1", - w).attr("x2", 2 * w)
+ .attr("y1", d => y(d.press)).attr("y2", d => y(d.press))
+ .attr("clip-path", "url(#clipper)")
+ .attr("class", d => `sigline ${d.classes}`);
+
+
+ //barbs stuff
+
+ let lastH = -300;
+ //filter barbs to be valid and not too crowded
+ const barbs = data4moving.filter(function (d) {
+ if (d.hght > lastH + steph && (d.wspd || d.wspd === 0) && d.press >= topp && !(d.wspd === 0 && d.wdir === 0)) lastH = d.hght;
+ return d.hght == lastH;
+ });
+
+ dataObj.lines.barbs = barbgroup.append("svg").attr("class", `barblines ${windDisplay=="Numerical"?"hidden":""}`);//.attr("transform","translate(30,80)");
+ dataObj.lines.barbs.selectAll("barbs")
+ .data(barbs).enter().append("use")
+ .attr("href", function (d) { return "#barb" + Math.round(convSpd(d.wspd, "kt") / 5) * 5; }) // 0,5,10,15,... always in kt
+ .attr("transform", function (d) { return "translate(" + (w + 15 * (ix + ixShift)) + "," + y(d.press) + ") rotate(" + (d.wdir + 180) + ")"; });
+
+
+ dataObj.lines.windtext = barbgroup.append("svg").attr("class", `windtext ${windDisplay=="Barbs"?"hidden":""}`);//.attr("class", "barblines");
+ dataObj.lines.windtext.selectAll("windtext")
+ .data(barbs).enter().append("g")
+ .attr("transform",d=> `translate(${w + 28 * (ix + ixShift) - 20} , ${y(d.press)})`);
+ dataObj.lines.windtext.selectAll("g").append("text")
+ .html( "↑" )
+ .style("transform",d=> "rotate(" + (180 + d.wdir)+"deg)");
+ dataObj.lines.windtext.selectAll("g").append("text")
+ .html( d=>Math.round(convSpd(d.wspd,"kt")))
+ .attr("x","0.5em");
+
+ ////clouds
+ const clouddata = clouds.computeClouds(data);
+ clouddata.canvas = _this["cloudRef" + (ix + ixShift + 1)];
+ clouds.cloudsToCanvas(clouddata);
+ dataObj.cloudCanvas = clouddata.canvas;
+ //////
+
+ if (select || dataAr.length == 1) {
+ selectSkewt(dataObj.data);
+ }
+ shiftXAxis();
+
+ return dataAr.length;
+ };
+
+
+ //// controls at bottom
+
+ var buttons = { "Dry Adiabat": {}, "Moist Adiabat": {}, "Isohume": {}, "Temp": {}, "Pressure": {} };
+ for (const p in buttons) {
+ const b = buttons[p];
+ b.hi = false;
+ b.el = controls.append("div").attr("class", "buttons").text(p).on("click", () => {
+ b.hi = !b.hi;
+ b.el.node().classList[b.hi ? "add" : "remove"]("clicked");
+ const line = p.replace(" ", "").toLowerCase();
+ lines[line]._groups[0].forEach(p => p.classList[b.hi ? "add" : "remove"]("highlight-line"));
+ });
+ } this.refs.highlightButtons = controls.node();
+
+ //values
+ const values = {
+ "surface": {},
+ "LCL": { hi: true },
+ "CCL": { hi: true },
+ "TCON": { hi: false },
+ "THRM top": { hi: false },
+ "CLD top": { hi: false }
+ };
+
+ for (const prop in values) {
+ const p = prop;
+ const b = values[p];
+ b.val = valuesContainer.append("div").attr("class", `buttons ${p == "surface" ? "noclick" : ""} ${b.hi ? "clicked" : ""}`).html(p + ":");
+ if (/CCL|LCL|TCON|THRM top|CLD top/.test(p)) {
+ b.val.on("click", () => {
+ b.hi = !b.hi;
+ b.val.node().classList[b.hi ? "add" : "remove"]("clicked");
+ selectedSkewt.lines[p]._groups[0].forEach(p => p.classList[b.hi ? "add" : "remove"]("highlight-line"));
+ });
+ }
+ }
+ this.refs.valueButtons = valuesContainer.node();
+
+ const ranges = {
+ parctemp: { value: 10, step: 0.1, min: -50, max: 50 },
+ topp: { min: 50, max: 900, step: 25, value: topp },
+ parctempShift: { min: -5, step: 0.1, max: 10, value: parctempShift },
+ gradient: { min: 0, max: 85, step: 1, value: gradient },
+ // midtemp:{value:0, step:2, min:-50, max:50},
+
+ };
+
+ const unit4range = p => p == "gradient" ? "°" : p == "topp" ? "hPa" : "°C";
+
+ const html4range = (v, p) => {
+ let html = "";
+ if (p == "parctempShift" && r.value >= 0) html += "+";
+ html += (p == "gradient" || p == "topp" ? Math.round(v) : Math.round(v * 10) / 10) + unit4range(p);
+ if (p == "parctemp") {
+ const shift = selectedSkewt ? (Math.round((v - selectedSkewt.data[0].temp) * 10) / 10) : parctempShift;
+ html += " " + (shift > 0 ? "+" : "") + shift + "";
+ }
+ return html;
+ };
+
+ for (const prop in ranges) {
+ const p = prop;
+ const contnr = p == "parctemp" || p == "topp" ? rangeContainer : rangeContainer2;
+ const r = ranges[p];
+ r.row=contnr.append("div").attr("class","row"); this.refs[p]=r.row.node();
+ r.valueDiv = r.row.append("div").attr("class", "skewt-range-des").html(p == "gradient" ? "Gradient:" : p == "topp" ? "Top P:" : p == "parctemp" ? "Parcel T:" : "Parcel T Shift:");
+ r.valueDiv = r.row.append("div").attr("class", "skewt-range-val").html(html4range(r.value, p));
+ r.input = r.row.append("input").attr("type", "range").attr("min", r.min).attr("max", r.max).attr("step", r.step).attr("value", p == "gradient" ? 90 - r.value : r.value).attr("class", "skewt-ranges")
+ .on("input", (a, b, c) => {
+
+ _this.hideTooltips();
+ r.value = +c[0].value;
+
+ if (p == "gradient") {
+ gradient = r.value = 90 - r.value;
+ showErlFor2Sec(0, 0, r.input);
+ //console.log("GRADIENT ST", gradient);
+ }
+ if (p == "topp") {
+ showErlFor2Sec(0, 0, r.input);
+ const h_oldtopp = y(basep) - y(topp);
+ topp = r.value;
+ const h_newtopp = y(basep) - y(topp);
+ pIncrement = topp > 500 ? -25 : -50;
+ if (adjustGradient) {
+ ranges.gradient.value = gradient = Math.atan(Math.tan(gradient * deg2rad) * h_oldtopp / h_newtopp) / deg2rad;
+ ranges.gradient.input.node().value = 90 - gradient; //will trigger input event anyway
+ ranges.gradient.valueDiv.html(html4range(gradient, "gradient"));
+ init_temprange*=h_oldtopp/h_newtopp;
+ if (ranges.gradient.cbfs) ranges.gradient.cbfs.forEach(cbf => cbf(gradient));
+ }
+ steph = atm.getElevation(topp) / 30;
+ }
+ if (p == "parctempShift") {
+ parctempShift = r.value;
+ }
+
+ r.valueDiv.html(html4range(r.value, p));
+
+ clearTimeout(moving);
+ moving = setTimeout(() => {
+ moving = false;
+ if (p == "parctemp") {
+ if (selectedSkewt) drawParcelTraj(selectedSkewt); //value already set
+ } else {
+ resize();
+ }
+ }, 1000);
+
+ if (p == "parctemp"){
+ if (selectedSkewt) {
+ selectedSkewt.parctemp = r.value;
+ drawParcelTraj(selectedSkewt);
+ }
+ } else {
+ resize();
+ }
+
+ //this.cbfRange({ topp, gradient, parctempShift });
+ if (r.cbfs) r.cbfs.forEach(cbf => cbf(p=="gradient"? gradient: r.value));
+ });
+
+ //contnr.append("div").attr("class", "flex-break");
+ }
+
+
+ let showElr;
+ const showErlFor2Sec = (a, b, target) => {
+ target = target[0] || target.node();
+ lines.elr.classed("highlight-line", true);
+ clearTimeout(showElr);
+ showElr = null;
+ showElr = setTimeout(() => {
+ target.blur();
+ lines.elr.classed("highlight-line", showElr = null); //background may be drawn again
+ }, 1000);
+ };
+
+ ranges.gradient.input.on("focus", showErlFor2Sec);
+ ranges.topp.input.on("focus", showErlFor2Sec);
+
+ const cbSpan = rangeContainer2.append("span").attr("class", "row checkbox-container");
+ this.refs.maintainXCheckBox = cbSpan.node();
+ cbSpan.append("input").attr("type", "checkbox").on("click", (a, b, e) => {
+ adjustGradient = e[0].checked;
+ });
+ cbSpan.append("span").attr("class", "skewt-checkbox-text").html("Maintain temp range on X-axis when zooming");
+
+ const selectUnits = rangeContainer2.append("div").attr("class", "row select-units");
+ this.refs.selectUnits = selectUnits.node();
+ selectUnits.append("div").style("width","10em").html("Select alt units: ");
+ const units = { "meter": {}, "feet": {} };
+ for (const prop in units) {
+ const p = prop;
+ units[p].hi = p[0] == unitAlt;
+ units[p].el = selectUnits.append("div").attr("class", "buttons units" + (unitAlt == p[0] ? " clicked" : "")).text(p).on("click", () => {
+ for (const p2 in units) {
+ units[p2].hi = p == p2;
+ units[p2].el.node().classList[units[p2].hi ? "add" : "remove"]("clicked");
+ }
+ unitAlt = p[0];
+ if (currentY !== null) _this.move2P(currentY);
+ drawParcelTraj(selectedSkewt);
+ });
+ }
+ const selectWindDisp = rangeContainer2.append("div").attr("class", "row select-units");
+ this.refs.selectWindDisp = selectWindDisp.node();
+ selectWindDisp.append("div").style("width","10em").html("Select wind display: ");
+ const windDisp = { "Barbs": {}, "Numerical": {} };
+ for (const prop in windDisp) {
+ const p = prop;
+ windDisp[p].hi = p == windDisplay;
+ windDisp[p].el = selectWindDisp.append("div").attr("class", "buttons units" + (windDisplay == p ? " clicked" : "")).text(p).on("click", () => {
+ for (const p2 in windDisp) {
+ windDisp[p2].hi = p == p2;
+ windDisp[p2].el.node().classList[windDisp[p2].hi ? "add" : "remove"]("clicked");
+ }
+ windDisplay = p;
+ //console.log(windDisplay);
+ dataAr.forEach(d=>{
+ d.lines.barbs.classed("hidden", windDisplay=="Numerical");
+ d.lines.windtext.classed("hidden", windDisplay=="Barbs");
+ });
+ });
+ }
+ const removePlot = (s) => { //remove single plot
+ const dataObj = dataAr.find(d => d.data == s);
+ //console.log(dataObj);
+ if (!dataObj) return;
+ let ix=dataAr.indexOf(dataObj);
+ //clear cloud canvas.
+ if (dataObj.cloudCanvas){
+ const ctx = dataObj.cloudCanvas.getContext("2d");
+ ctx.clearRect(0, 0, 10, 200);
+ }
+
+ for (const p in dataObj.lines) {
+ dataObj.lines[p].remove();
+ }
+ dataAr.splice(ix, 1);
+ if(dataAr.length==0) {
+ _this.hideTooltips();
+ console.log("All plots removed");
+ }
+ };
+
+ const clear = () => { //remove all plots and data
+ dataAr.forEach(d => {
+ for (const p in d.lines) d.lines[p].remove();
+ const ctx = d.cloudCanvas.getContext("2d");
+ ctx.clearRect(0, 0, 10, 200);
+ });
+ _this.hideTooltips();
+ // these maybe not required, addressed by above.
+ skewtgroup.selectAll("lines").remove();
+ skewtgroup.selectAll("path").remove(); //clear previous paths from skew
+ skewtgroup.selectAll("g").remove();
+ barbgroup.selectAll("use").remove(); //clear previous paths from barbs
+ dataAr = [];
+ //if(tooltipRect)tooltipRect.remove(); tooltip rect is permanent
+ };
+
+ const clearBg = () => {
+ skewtbg.selectAll("*").remove();
+ };
+
+ const setParams = (p) => {
+ ({ height=height, topp=topp, parctempShift=parctempShift, parctemp=parctemp, basep=basep, steph=steph, gradient=gradient } = p);
+ if (p=="gradient") ranges.gradient.input.value = 90 - p;
+ else if (ranges[p]) ranges[p].input.value = p;
+ //resize();
+ };
+
+ const getParams = () =>{
+ return {height, topp, basep, steph, gradient, parctempShift, parctemp: selectSkewt.parctemp }
+ };
+
+ const shiftDegrees = function (d) {
+ xOffset = x(0) - x(d) ;
+ //console.log("xOffs", xOffset);
+ shiftXAxis();
+ };
+
+
+ // Event cbfs.
+ // possible events: temp, press, parctemp, topp, parctempShift, gradient;
+
+ const pressCbfs=[];
+ const tempCbfs=[];
+ const on = (ev, cbf) =>{
+ let evAr;
+ if (ev=="press" || ev=="temp") {
+ evAr=ev=="press"?pressCbfs:tempCbfs;
+ } else {
+ for (let p in ranges) {
+ if(ev.toLowerCase() == p.toLowerCase()){
+ if (!ranges[p].cbfs) ranges[p].cbfs = [];
+ evAr=ranges[p].cbfs;
+ }
+ }
+ }
+ if (evAr){
+ if (!evAr.includes(cbf)) {
+ evAr.push(cbf);
+ } else {
+ console.log("EVENT ALREADY REGISTERED");
+ }
+ } else {
+ console.log("EVENT NOT RECOGNIZED");
+ }
+ };
+
+ const off = (ev, cbf) => {
+ let evAr;
+ if (ev=="press" || ev=="temp") {
+ evAr=ev=="press"?pressCbfs:tempCbfs;
+ } else {
+ for (let p in ranges) {
+ if(ranges[p].cbfs && ev.toLowerCase() == p.toLowerCase()){
+ evAr=ranges[p].cbfs;
+ }
+ }
+ }
+ if (evAr) {
+ let ix = evAr.findIndex(c=>cbf==c);
+ if (ix>=0) evAr.splice(ix,1);
+ }
+ };
+
+ // Add functions as public methods
+
+ this.drawBackground = drawBackground;
+ this.resize = resize;
+ this.plot = plot;
+ this.clear = clear; //clear all the plots
+ this.clearBg = clearBg;
+ this.selectSkewt = selectSkewt;
+ this.removePlot = removePlot; //remove a specific plot, referenced by data object passed initially
+
+ this.on = on;
+ this.off = off;
+ this.setParams = setParams;
+ this.getParams = getParams;
+ this.shiftDegrees = shiftDegrees;
+
+ /**
+ * parcelTrajectory:
+ * @param params = {temp, gh, level},
+ * @param {number} steps,
+ * @param surfacetemp, surf pressure and surf dewpoint
+ */
+ this.parcelTrajectory = atm.parcelTrajectory;
+
+ this.pressure2y = y;
+ this.temp2x = x;
+ this.gradient = gradient; //read only, use setParams to set.
+
+ // this.move2P, this.hideTooltips, this.showTooltips, has been declared
+
+ // this.cloudRef1 and this.cloudRef2 = references to the canvas elements to add clouds with other program
+
+ this.refs.tooltipRect = tooltipRect.node();
+
+ /* other refs:
+ highlightButtons
+ valueButtons
+ parctemp
+ topp
+ gradient
+ parctempShift
+ maintainXCheckBox
+ selectUnits
+ selectWindDisp
+ tooltipRect
+ */
+
+ //init
+ setVariables();
+ resize();
+ drawToolTips.call(this); //only once
+ makeBarbTemplates(); //only once
+};}());
\ No newline at end of file
diff --git a/js/tracker.js b/js/tracker.js
index 15c450a..1fb47fa 100644
--- a/js/tracker.js
+++ b/js/tracker.js
@@ -36,6 +36,8 @@ var stationHistoricalData = {};
var historicalPlots = {};
var historicalAjax = [];
+var skewtdata = [];
+
var sites = null;
var launches = new L.LayerGroup();
var showLaunches = false;
@@ -296,10 +298,73 @@ function calculate_lookangles(a, b) {
'elevation': elevation,
'azimuth': bearing,
'range': distance,
+ 'great_circle_distance': great_circle_distance,
'bearing': str_bearing
};
}
+function getPressure(altitude){
+
+ // Constants
+ airMolWeight = 28.9644; // Molecular weight of air
+ densitySL = 1.225; // Density at sea level [kg/m3]
+ pressureSL = 101325; // Pressure at sea level [Pa]
+ temperatureSL = 288.15; // Temperature at sea level [deg K]
+ gamma = 1.4;
+ gravity = 9.80665; // Acceleration of gravity [m/s2]
+ tempGrad = -0.0065; // Temperature gradient [deg K/m]
+ RGas = 8.31432; // Gas constant [kg/Mol/K]
+ R = 287.053;
+ deltaTemperature = 0.0;
+
+ // Lookup Tables
+ altitudes = [0, 11000, 20000, 32000, 47000, 51000, 71000, 84852];
+ pressureRels = [
+ 1,
+ 2.23361105092158e-1,
+ 5.403295010784876e-2,
+ 8.566678359291667e-3,
+ 1.0945601337771144e-3,
+ 6.606353132858367e-4,
+ 3.904683373343926e-5,
+ 3.6850095235747942e-6,
+ ];
+ temperatures = [288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.946];
+ tempGrads = [-6.5, 0, 1, 2.8, 0, -2.8, -2, 0];
+ gMR = gravity * airMolWeight / RGas;
+
+ // Pick a region to work in
+ i = 0;
+ if (altitude > 0){
+ while (altitude > altitudes[i + 1]){
+ i = i + 1;
+ }
+ }
+
+ // Lookup based on region
+ baseTemp = temperatures[i];
+ tempGrad = tempGrads[i] / 1000.0;
+ pressureRelBase = pressureRels[i];
+ deltaAltitude = altitude - altitudes[i];
+ temperature = baseTemp + tempGrad * deltaAltitude;
+
+ // Calculate relative pressure
+ if(Math.abs(tempGrad) < 1e-10){
+ pressureRel = pressureRelBase * Math.exp(
+ -1 * gMR * deltaAltitude / 1000.0 / baseTemp
+ );
+ } else{
+ pressureRel = pressureRelBase * Math.pow(
+ baseTemp / temperature, gMR / tempGrad / 1000.0
+ );
+ }
+
+ // Finally, work out the pressure
+ pressure = pressureRel * pressureSL;
+
+ return pressure/100.0; // Return pressure in hPa
+}
+
function update_lookangles(vcallsign) {
if(GPS_ts === null) { return; }
else if($("#lookanglesbox span").first().is(":hidden")) {
@@ -520,6 +585,7 @@ function load() {
zoomAnimationThreshold: 0,
center: [53.467511,-2.233894],
layers: baseMaps[selectedLayer],
+ worldCopyJump: true,
preferCanvas: true,
});
@@ -913,7 +979,6 @@ function drawHistorical (data, station) {
}
html += "";
- html += "";
html += "
";
html += ""
@@ -2269,7 +2334,7 @@ function updateVehicleInfo(vcallsign, newPosition) {
'

' +
'
Path' +
((vehicle.vehicle_type!="car") ? '
Share' : '') +
- ((vehicle.vehicle_type!="car") ? '
Plot' : '') +
+ ((vehicle.vehicle_type!="car") ? '
SkewT' : '') +
'
' +
'
';
//mobile
@@ -2281,7 +2346,7 @@ function updateVehicleInfo(vcallsign, newPosition) {
'
' +
'Path' +
((vehicle.vehicle_type!="car") ? 'Share' : '') +
- ((vehicle.vehicle_type!="car") ? 'Plot' : '') +
+ ((vehicle.vehicle_type!="car") ? 'SkewT' : '') +
'' +
'
';
var b = '
' +
@@ -2335,6 +2400,215 @@ function updateVehicleInfo(vcallsign, newPosition) {
return true;
}
+function skewTdelete () {
+ var box = $("#skewtbox");
+
+ skewt.clear();
+ $('#resetSkewt').hide();
+ $('#deleteSkewt').hide();
+ $("#skewtSerial").text("Select a Radiosonde from the list and click 'SkewT' to plot. Note that not all radiosonde types are supported.");
+ box.hide();
+ //$('.skewt').hide();
+ $("#skewt-plot").empty();
+ checkSize();
+}
+
+function skewTrefresh () {
+ skewt.clear();
+ $("#skewt-plot").empty();
+ $('#resetSkewt').hide();
+ $('#deleteSkewt').hide();
+
+ skewt = new SkewT('#skewt-plot');
+
+ try {
+ skewt.plot(skewtdata);
+ $('#resetSkewt').show();
+ $('#deleteSkewt').show();
+ }
+ catch(err) {}
+}
+
+function skewTdraw (callsign) {
+ // Open sidebar
+ var box = $("#skewtbox");
+
+ if(box.is(':hidden')) {
+ $('.flatpage, #homebox').hide();
+ $('.skewt').show();
+ box.show().scrollTop(0);
+ checkSize();
+ };
+
+ // Delete existing
+ try {
+ skewt.clear();
+ } catch (err) {}
+
+ $('#resetSkewt').hide();
+ $('#deleteSkewt').hide();
+ $("#skewt-plot").empty();
+ $("#skewtErrors").text("");
+ $("#skewtErrors").hide();
+
+ // Loading gif
+ $("#skewtLoading").show();
+ $("#skewtSerial").show();
+ $("#skewtSerial").text("Serial: " + callsign);
+
+ // Download Data
+ var data_url = "https://api.v2.sondehub.org/sonde/" + encodeURIComponent(callsign);
+ $.ajax({
+ type: "GET",
+ url: data_url,
+ dataType: "json",
+ success: function(data) {
+ processSkewT(data);
+ }
+ });
+
+ // Credit https://github.com/projecthorus/sondehub-card/blob/main/js/utils.js#L116
+ function processSkewT (data) {
+ burst_idx = -1;
+ max_alt = -99999.0;
+ for (let i = 0; i < data.length; i++){
+ alt = parseFloat(data[i].alt);
+ if (alt > max_alt){
+ max_alt = alt;
+ burst_idx = i;
+ }
+ }
+ if(data.length < 50){
+ $("#skewtErrors").text("Insufficient data for Skew-T plot (<50 points).");
+ $("#skewtErrors").show();
+ return;
+ }
+
+ // Check that we have ascent data
+ if (burst_idx <= 0){
+ $("#skewtErrors").text("Insufficient data for Skew-T plot (Only descent data available).");
+ $("#skewtErrors").show();
+ return;
+ }
+
+ // Check that the first datapoint is at a reasonable altitude.
+ if (data[0].alt > 15000){
+ $("#skewtErrors").text("Insufficient data for Skew-T plot (Only data > 15km available)");
+ $("#skewtErrors").show();
+ return;
+ }
+
+ v1_data = false;
+ sonde_type = data[data.length-1].type;
+ if(sonde_type == 'payload_telemetry'){
+ // Sondehub v1 data.
+ v1_data = true;
+ }
+
+ var skewt_data = [];
+ decimation = 25;
+ if (v1_data == true){
+ decimation = 1;
+ }
+
+ idx = 1;
+
+ while (idx < burst_idx){
+ entry = data[idx];
+ old_entry = data[idx-1];
+
+ _old_date = new Date(old_entry.datetime);
+ _new_date = new Date(entry.datetime);
+ _time_delta = (_new_date - _old_date)/1000.0;
+ if (_time_delta <= 0){
+ idx = idx + 1;
+ continue;
+ }
+
+ _temp = null;
+ _dewp = -1000.0;
+ _pressure = null;
+
+ // Extract temperature datapoint
+ if (entry.hasOwnProperty('temp')){
+ if(parseFloat(entry.temp) > -270.0){
+ _temp = parseFloat(entry.temp);
+ } else{
+ idx = idx + 1;
+ continue;
+ }
+ }else{
+ // No temp data. Skip to the next point
+ idx = idx + 1;
+ continue;
+ }
+
+ // Try and extract RH datapoint
+ if (entry.hasOwnProperty('humidity')){
+ if(parseFloat(entry.humidity) >= 0.0){
+ _rh = parseFloat(entry.humidity);
+ // Calculate the dewpoint
+ _dewp = (243.04 * (Math.log(_rh / 100) + ((17.625 * _temp) / (243.04 + _temp))) / (17.625 - Math.log(_rh / 100) - ((17.625 * _temp) / (243.04 + _temp))));
+ } else {
+ _dewp = -1000.0;
+ }
+ }
+
+ // Calculate movement
+ _old_pos = {'lat': old_entry.lat, 'lon': old_entry.lon, 'alt': old_entry.alt};
+ _new_pos = {'lat': entry.lat, 'lon': entry.lon, 'alt': entry.alt};
+
+ _pos_info = calculate_lookangles(_old_pos, _new_pos);
+ _wdir = (_pos_info['azimuth']+180.0)%360.0;
+ _wspd = _pos_info['great_circle_distance']/_time_delta;
+
+ if (entry.hasOwnProperty('pressure')){
+ _pressure = entry.pressure;
+ } else {
+ // Otherwise, calculate it
+ _pressure = getPressure(_new_pos.alt);
+ }
+
+ if(_pressure < 50.0){
+ break;
+ }
+
+ _new_skewt_data = {"press": _pressure, "hght": _new_pos.alt, "temp": _temp, "dwpt": _dewp, "wdir": _wdir, "wspd": _wspd};
+
+ skewt_data.push(_new_skewt_data);
+
+ idx = idx + decimation;
+ }
+
+ skewtdata = skewt_data;
+
+ $("#skewtLoading").hide();
+
+ if (skewtdata.length > 0){
+
+ if(box.is(':hidden')) {
+ $('.flatpage, #homebox').hide();
+ $('.skewt').show();
+ box.show().scrollTop(0);
+ checkSize();
+ };
+
+ skewt = new SkewT('#skewt-plot');
+
+ try {
+ skewt.plot(skewtdata);
+ $('#resetSkewt').show();
+ $('#deleteSkewt').show();
+ }
+ catch(err) {}
+
+ } else {
+ $("#skewtErrors").show();
+ $("#skewtErrors").text("Insufficient Data available, or no Temperature/Humidity data available to generate Skew-T plot.");
+ };
+ }
+};
+
function set_polyline_visibility(vcallsign, val) {
var vehicle = vehicles[vcallsign];
vehicle.polyline_visible = val;
@@ -3575,7 +3849,7 @@ function addPosition(position) {
// Graph Stuff
-var graph_inhibited_fields = ['frequency', 'burst_timer'];
+var graph_inhibited_fields = ['frequency', 'frequency_tx', 'burst_timer'];
function updateGraph(vcallsign, reset_selection) {
if(!plot || !plot_open) return;