/**
* @module meteoJS/thermodynamicDiagram/tdDiagram
*/
import {
tempCelsiusToKelvin,
tempKelvinToCelsius,
potentialTempByTempAndPres,
saturationHMRByTempAndPres,
lclByPotentialTempAndHMR,
lclTemperatureByTempAndDewpoint,
equiPotentialTempByTempAndDewpointAndPres,
wetbulbTempByTempAndDewpointAndPres,
altitudeISAByPres
} from '../calc.js';
import {
getNormalizedLineStyleOptions,
getNormalizedFontOptions,
getFirstDefinedValue,
drawTextInto
} from './Functions.js';
import PlotAltitudeDataArea from './PlotAltitudeDataArea.js';
/**
* Object passed on events.
*
* @typedef {module:meteoJS/thermodynamicDiagram/plotArea~event}
* module:meteoJS/thermodynamicDiagram/tdDiagram~event
* @property {number} p - Pressure coordinate [hPa].
* @property {number} T - Temperature coordinate [K].
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#click
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#dblclick
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#mousedown
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#mouseup
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#mouseover
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#mouseout
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#mousemove
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#touchstart
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#touchmove
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#touchleave
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#touchend
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* @event module:meteoJS/thermodynamicDiagram/tdDiagram#touchcancel
* @type {module:meteoJS/thermodynamicDiagram/tdDiagram~event}
*/
/**
* Options for pressure label.
*
* @typedef {module:meteoJS/thermodynamicDiagram~lineTextOptions}
* module:meteoJS/thermodynamicDiagram/tdDiagram~presLabelOptions
* @property {string|Object} [fill]
* Fill option for background rect. Default is 'white' with opacity 0.7.
* @property {number} [horizontalMargin=5] - Margin in x direction.
* @property {number} [verticalMargin=0] - Margin in y direction.
* @property {number|'100%'} [length=60]
* Length of the horizontal line. A number is in pixel unit. A string
* with a appended '%' indicates a length relative to the diagram width.
* @property {'left'|'right'} [align='left']
* Align pressure label left/right in the diagram.
*/
/**
* Options for labels.
*
* @typedef {module:meteoJS/thermodynamicDiagram~lineTextOptions}
* module:meteoJS/thermodynamicDiagram/tdDiagram~labelsOptions
* @property {string|Object} [fill]
* Fill option for background rect. Default is 'white' with opacity 0.7.
* @property {number} [horizontalMargin=10] - Margin in x direction.
* @property {number} [verticalMargin=0] - Margin in y direction.
* @property {number} [radius=undefined] - Radius for hover circle.
* @property {number} [radiusPlus=2]
* Radius relative to line width for hover circle.
*/
/**
* Options for labels on hovering the thermodynamic diagram.
*
* @typedef {module:meteoJS/thermodynamicDiagram/plotAltitudeDataArea~hoverLabelsOptions}
* module:meteoJS/thermodynamicDiagram/tdDiagram~hoverLabelsOptions
* @property {module:meteoJS/thermodynamicDiagram/tdDiagram~presLabelOptions}
* [pres] - Options for pressure label.
* @property {module:meteoJS/thermodynamicDiagram/tdDiagram~labelsOptions}
* [temp] - Options for temperature label.
* @property {module:meteoJS/thermodynamicDiagram/tdDiagram~labelsOptions}
* [dewp] - Options for dew point label.
* @property {module:meteoJS/thermodynamicDiagram/tdDiagram~labelsOptions}
* [wetbulb] - Options for wetbulb temperature label.
*/
/**
* Options for parcels in the diagram.
*
* @typedef {Object}
* module:meteoJS/thermodynamicDiagram/tdDiagram~parcelsOptions
* @property {boolean} [visible=true] - Visibility of parcels.
*/
/**
* Definition of lines in a thermodynamic diagram.
*
* @typedef {module:meteoJS/thermodynamicDiagram~lineStyleOptions}
* module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions
* @property {undefined|Array<number>} [highlightedLines=undefined]
* Highlight lines at this values.
* @property {undefined|Array<number>} [lines=undefined]
* Draw values for this values.
* @property {number} [max=undefined]
* Maximum value for a line. Ignored if lines is set.
* @property {number} [min=undefined]
* Minimum value for a line. Ignored if lines is set.
* @property {number} [interval=undefined]
* Interval between different lines. Ignored if lines is set.
* @property {number} [maxPressure=undefined]
* Start line from this maximum pressure.
* @property {number} [minPressure=undefined]
* End line at this minimum pressure.
*/
/**
* Options for the constructor.
*
* @typedef {module:meteoJS/thermodynamicDiagram/plotAltitudeDataArea~options}
* module:meteoJS/thermodynamicDiagram/tdDiagram~options
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions}
* [isobars] - Isobars configuration.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions}
* [isotherms] - Isotherms configuration.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions}
* [dryadiabats] - Dry adiabats configuration.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions}
* [pseudoadiabats] - Pseudo adiabats configuration.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions}
* [mixingratio] - Mixing ratio configuration.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~hoverLabelsOptions}
* [hoverLabels] - Hover labels options.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~parcelsOptions}
* [parcels] - Parcels options.
*/
/**
* Class to draw the real thermodynamic diagram.
*
* <pre><code>import TDDiagram from 'meteojs/thermodynamicDiagram/TDDiagram';</code></pre>
*
* @extends module:meteoJS/thermodynamicDiagram/plotAltitudeDataArea.PlotAltitudeDataArea
*
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#click
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#dblclick
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#mousedown
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#mouseup
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#mouseover
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#mouseout
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#mousemove
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#touchstart
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#touchmove
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#touchleave
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#touchend
* @fires module:meteoJS/thermodynamicDiagram/tdDiagram#touchcancel
*/
export class TDDiagram extends PlotAltitudeDataArea {
/**
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~linesOptions} [options]
* Options.
*/
constructor({
svgNode = undefined,
coordinateSystem = undefined,
x = 0,
y = 0,
width = 100,
height = 100,
style = {},
visible = true,
events = {},
dataGroupIds = ['temp', 'dewp', 'wetbulb'],
getCoordinatesByLevelData = (dataGroupId, sounding, levelData, plotArea) => {
if (levelData.pres === undefined)
return {};
let value = undefined;
switch (dataGroupId) {
case 'temp':
value = levelData.tmpk;
break;
case 'dewp':
value = levelData.dwpk;
break;
case 'wetbulb':
value = wetbulbTempByTempAndDewpointAndPres(
levelData.tmpk,
levelData.dwpk,
levelData.pres
);
break;
}
if (value === undefined)
return {};
return {
x: plotArea.coordinateSystem.getXByPT(levelData.pres, value),
y: plotArea.coordinateSystem.height -
plotArea.coordinateSystem.getYByPT(levelData.pres, value),
value: Math.round(tempKelvinToCelsius(value)*10)/10,
unit: '℃'
};
},
insertDataGroupInto = (svgNode, dataGroupId, sounding, data) => {
if (dataGroupId in sounding.options.diagram
&& !sounding.options.diagram[dataGroupId].visible)
return;
const options =
(dataGroupId in sounding.options.diagram)
? sounding.options.diagram[dataGroupId].style : {};
svgNode.group()
.polyline(data.map(level => [ level.x, level.y ]))
.fill('none').stroke(options);
},
filterDataPoint = undefined,
minDataPointsDistance = 0,
isobars = {},
isotherms = {},
dryadiabats = {},
pseudoadiabats = {},
mixingratio = {},
hoverLabels = {},
parcels = {}
} = {}) {
super({
svgNode,
coordinateSystem,
x,
y,
width,
height,
style,
visible,
events,
hoverLabels,
getSoundingVisibility:
sounding => sounding.visible && sounding.options.diagram.visible,
dataGroupIds,
getCoordinatesByLevelData,
insertDataGroupInto,
filterDataPoint,
minDataPointsDistance
});
this.options = {
isobars: getNormalizedDiagramLineOptions(isobars),
isotherms:
getNormalizedDiagramLineOptions(isotherms, {
highlightedLines: [tempCelsiusToKelvin(0)]
}),
dryadiabats:
getNormalizedDiagramLineOptions(dryadiabats),
pseudoadiabats:
getNormalizedDiagramLineOptions(pseudoadiabats, {
style: {
color: 'rgb(102, 51, 0)',
dasharray: 6
}
}),
mixingratio:
getNormalizedDiagramLineOptions(mixingratio, {
minPressure: 500,
style: {
color: 'rgb(102, 51, 0)',
dasharray: 2
}
})
};
this.svgGroups = {
border: this._svgNodeBackground.group(),
isobars: this._svgNodeBackground.group(),
isotherms: this._svgNodeBackground.group(),
dryadiabats: this._svgNodeBackground.group(),
mixingratio: this._svgNodeBackground.group(),
pseudoadiabats: this._svgNodeBackground.group()
};
/**
* @type module:meteoJS/thermodynamicDiagram/tdDiagram~parcelsOptions
* @private
*/
this._parcelsOptions = parcels;
if (!('visible' in this._parcelsOptions))
this._parcelsOptions.visible = true;
/**
* @typedef {module:meteoJS/thermodynamicDiagram/diagramSounding~parcelsItems}
* @property {undefined|external:SVG} parcelsGroup
* SVG Group to plot the parcels.
* @property {Map.<module:meteoJS/thermodynamicDiagram/diagramParcel.DiagramParcel,external:SVG>} parcelsGroup
* Pairs of DiagramParcel objects and SVG Group. The parcel is plotted
* into the group. The group is contained in 'parcelsGroup'.
* @property {undefined|mixed} addItemListenerKey
* Listener key for the {@link module:meteoJS/base/collection#add:item} event
* on {@link module:meteoJS/thermodynamicDiagram/diagramSounding.DiagramSounding#diagramParcelCollection}
* for each sounding plotted in this diagram.
* @property {Object[]} changeVisibleListeners
* @property {Object[]} changeOptionsListeners
*/
/**
* @type Map.<module:meteoJS/thermodynamicDiagram/diagramSounding.DiagramSounding,module:meteoJS/thermodynamicDiagram/diagramSounding~parcelsItems>
* @private
*/
this._parcels = new Map();
this.on('add:sounding', sounding => {
/** @type module:meteoJS/thermodynamicDiagram/diagramSounding~parcelsItems */
const soundingParcelsItems = {
parcelsGroup: undefined,
parcelsGroups: new Map(),
addItemListenerKey: undefined,
removeItemListenerKey: undefined,
changeVisibleListeners: [],
changeOptionsListeners: []
};
const onAddParcel = diagramParcel => {
soundingParcelsItems.changeVisibleListeners.push({
diagramParcel,
listenerKey: diagramParcel.on('change:visible', () => {
if (!soundingParcelsItems.parcelsGroups.has(diagramParcel))
return;
const group = soundingParcelsItems.parcelsGroups.get(diagramParcel);
diagramParcel.visible ? group.show() : group.hide();
})
});
soundingParcelsItems.changeOptionsListeners.push({
diagramParcel,
listenerKey: diagramParcel.on('change:options', () => {
// Delte old parcel
const soundingParcelsItems = this._parcels.get(sounding);
if (soundingParcelsItems !== undefined) {
const group =
soundingParcelsItems.parcelsGroups.get(diagramParcel);
if (group !== undefined) {
soundingParcelsItems.parcelsGroups.delete(diagramParcel);
group.remove();
}
}
// Redraw
this.drawParcel(sounding, diagramParcel);
})
});
};
soundingParcelsItems.addItemListenerKey =
sounding.diagramParcelCollection.on('add:item', diagramParcel => {
onAddParcel(diagramParcel);
this.drawParcel(sounding, diagramParcel);
});
soundingParcelsItems.removeItemListenerKey =
sounding.diagramParcelCollection.on('remove:item', diagramParcel => {
const group =
soundingParcelsItems.parcelsGroups.get(diagramParcel);
if (group !== undefined) {
soundingParcelsItems.parcelsGroups.delete(diagramParcel);
group.remove();
}
});
for (let diagramParcel of sounding.diagramParcelCollection)
onAddParcel(diagramParcel);
this._parcels.set(sounding, soundingParcelsItems);
/* After this event, {@link module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram#drawSounding}
* is executed and therefore also
* {@link module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram#drawParcels}.
*/
});
// Remove all listeners on the parcels contained in the removed sounding.
this.on('remove:sounding', sounding => {
if (this._parcels.has(sounding)) {
/** @type module:meteoJS/thermodynamicDiagram/diagramSounding~parcelsItems */
const soundingParcelsItems = this._parcels.get(sounding);
sounding.diagramParcelCollection
.un('add:item', soundingParcelsItems.addItemListenerKey);
sounding.diagramParcelCollection
.un('remove:item', soundingParcelsItems.removeItemListenerKey);
soundingParcelsItems.changeVisibleListeners
.forEach(listenerObj =>
listenerObj.diagramParcel
.un('change:visible', listenerObj.listenerKey));
soundingParcelsItems.changeOptionsListeners
.forEach(listenerObj =>
listenerObj.diagramParcel
.un('change:options', listenerObj.listenerKey));
}
this._parcels.delete(sounding);
});
this.init();
}
/**
* Return the visibility of the isobars.
* @returns {boolean} Visibility of the isobars.
* @deprecated
*/
getIsobarsVisible() {
return this.options.isobars.visible;
}
/**
* Sets the visibility of the isobars.
* @param {boolean} visible Visibility of the isobars.
* @returns {module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram} this.
* @deprecated
*/
setIsobarsVisible(visible) {
this.options.isobars.visible = visible ? true : false;
this.plotIsobars();
return this;
}
/**
* Return the visibility of the isotherms.
* @returns {boolean} Visibility of the isotherms.
* @deprecated
*/
getIsothermsVisible() {
return this.options.isotherms.visible;
}
/**
* Sets the visibility of the isotherms.
* @param {boolean} visible Visibility of the isotherms.
* @returns {module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram} this.
* @deprecated
*/
setIsothermsVisible(visible) {
this.options.isotherms.visible = visible ? true : false;
this.plotIsotherms();
return this;
}
/**
* Return the visibility of the dry adiabats.
* @returns {boolean} Visibility of the dry adiabats.
* @deprecated
*/
getDryadiabatsVisible() {
return this.options.dryadiabats.visible;
}
/**
* Sets the visibility of the dry adiabats.
* @param {boolean} visible Visibility of the dry adiabats.
* @returns {module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram} this.
* @deprecated
*/
setDryadiabatsVisible(visible) {
this.options.dryadiabats.visible = visible ? true : false;
this.plotDryadiabats();
return this;
}
/**
* Return the visibility of the pseudo adiabats.
* @returns {boolean} Visibility of the pseudo adiabats.
* @deprecated
*/
getPseudoadiabatsVisible() {
return this.options.pseudoadiabats.visible;
}
/**
* Sets the visibility of the pseudo adiabats.
* @param {boolean} visible Visibility of the pseudo adiabats.
* @returns {module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram} this.
* @deprecated
*/
setPseudoadiabatsVisible(visible) {
this.options.pseudoadiabats.visible = visible ? true : false;
this.plotPseudoadiabats();
return this;
}
/**
* Return the visibility of the mixing ratio.
* @returns {boolean} Visibility of the mixing ratio.
* @deprecated
*/
getMixingratioVisible() {
return this.options.mixingratio.visible;
}
/**
* Sets the visibility of the mixing ratio.
* @param {boolean} visible Visibility of the mixing ratio.
* @returns {module:meteoJS/thermodynamicDiagram/tdDiagram.TDDiagram} this.
* @deprecated
*/
setMixingratioVisible(visible) {
this.options.mixingratio.visible = visible ? true : false;
this.plotMixingratio();
return this;
}
/**
* Draw the sounding into the SVG group.
*
* @override
*/
drawSounding(sounding, group) {
super.drawSounding(sounding, group);
// Draw parcels
if (this._parcels.has(sounding)) {
let parcelsObj = this._parcels.get(sounding);
parcelsObj.parcelsGroup = group.group();
if (!sounding.options.parcels.visible)
parcelsObj.parcelsGroup.hide();
this._parcels.set(sounding, parcelsObj);
}
this.drawParcels(sounding);
}
/**
* Draws parcels of a sounding.
*
* @param {module:meteoJS/thermodynamicDiagram/diagramSounding.DiagramSounding}
* sounding - Sounding.
*/
drawParcels(sounding) {
if (!this._parcelsOptions.visible)
return;
if (!this._parcels.has(sounding))
return;
/** @type module:meteoJS/thermodynamicDiagram/diagramSounding~parcelsItems */
const soundingParcelsItems = this._parcels.get(sounding);
soundingParcelsItems.parcelsGroup.clear();
soundingParcelsItems.parcelsGroups.clear();
for (let diagramParcel of sounding.diagramParcelCollection)
this.drawParcel(sounding, diagramParcel);
}
/**
* Draws a parcel.
*
* @param {module:meteoJS/thermodynamicDiagram/diagramSounding.DiagramSounding}
* diagramSounding - DiagramSounding object, which contains the parcel.
* @param {module:meteoJS/thermodynamicDiagram/diagramParcel.DiagramParcel}
* diagramParcel - Parcel lift to draw.
* @param {external:SVG} group - SVG group to draw parcel into.
* @private
*/
drawParcel(diagramSounding, diagramParcel) {
const parcel = diagramParcel.parcel;
if (parcel.pres === undefined ||
parcel.tmpc === undefined ||
parcel.dwpc === undefined)
return;
if (!this._parcels.has(diagramSounding))
return;
const soundingParcelsItems = this._parcels.get(diagramSounding);
const group = soundingParcelsItems.parcelsGroup.group();
soundingParcelsItems.parcelsGroups.set(diagramParcel, group);
this._parcels.set(diagramSounding, soundingParcelsItems);
const pottmpk =
potentialTempByTempAndPres(tempCelsiusToKelvin(parcel.tmpc), parcel.pres);
const hmr =
saturationHMRByTempAndPres(tempCelsiusToKelvin(parcel.dwpc), parcel.pres);
const lclpres = lclByPotentialTempAndHMR(pottmpk, hmr);
const lcltmpk = lclTemperatureByTempAndDewpoint(
tempCelsiusToKelvin(parcel.tmpc),
tempCelsiusToKelvin(parcel.dwpc));
const lclthetaek = equiPotentialTempByTempAndDewpointAndPres(
lcltmpk, lcltmpk, lclpres);
const options = diagramParcel.options;
// SVG groups
if (!options.visible)
group.hide();
const tempGroup = group.group();
if (!options.temp.visible)
tempGroup.hide();
let dewpGroup = group.group();
if (!options.dewp.visible)
dewpGroup.hide();
// Draw temp curve
const yInterval = 10;
const y0 = this.coordinateSystem
.getYByPT(parcel.pres, tempCelsiusToKelvin(parcel.tmpc));
const x0 = this.coordinateSystem.getXByYPotentialTemperature(y0, pottmpk);
const y1 = this.coordinateSystem.getYByPPotentialTemperatur(lclpres, pottmpk);
const x1 = this.coordinateSystem.getXByYPotentialTemperature(y1, pottmpk);
let tempPolyline = [[x0, y0]];
if (!this.coordinateSystem.isDryAdiabatStraightLine())
for (let y=y0+yInterval; y<y1; y+=yInterval) {
tempPolyline.push([
this.coordinateSystem.getXByYPotentialTemperature(y, pottmpk),
y
]);
}
tempPolyline.push([x1, y1]);
const y2 = this.coordinateSystem.height;
const x2 = this.coordinateSystem.getXByYEquiPotTemp(y2, lclthetaek);
for (let y=y1+yInterval; y<y2; y+=yInterval) {
tempPolyline.push([
this.coordinateSystem.getXByYEquiPotTemp(y, lclthetaek),
y
]);
}
tempPolyline.push([x2, y2]);
tempGroup
.polyline(tempPolyline.map(point => {
point[1] = this.coordinateSystem.height - point[1];
return point;
}))
.fill('none')
.stroke(options.temp.style);
// Draw mixing ratio curve
const x0dwp = this.coordinateSystem.getXByYHMR(y0, hmr);
const x1dwp = this.coordinateSystem.getXByYHMR(y1, hmr);
let dewpPolyline = [[x0dwp, y0]];
for (let y=y0+yInterval; y<y1; y+=yInterval) {
dewpPolyline.push([
this.coordinateSystem.getXByYHMR(y, hmr),
y
]);
}
dewpPolyline.push([x1dwp, y1]);
dewpGroup
.polyline(dewpPolyline.map(point => {
point[1] = this.coordinateSystem.height - point[1];
return point;
}))
.fill('none')
.stroke(options.dewp.style);
}
/**
* Draw background into SVG group.
*
* @override
*/
_drawBackground(svgNode) {
super._drawBackground(svgNode);
this.svgGroups = {
border: svgNode.group(),
isobars: svgNode.group(),
isotherms: svgNode.group(),
dryadiabats: svgNode.group(),
mixingratio: svgNode.group(),
pseudoadiabats: svgNode.group()
};
// Rand des Diagramms
this.svgGroups.border.clear();
this.svgGroups.border
.rect(this.coordinateSystem.width, this.coordinateSystem.height)
.attr({stroke: 'black', 'stroke-width': 1, 'fill-opacity': 0});
// Hilfelinien zeichnen
this.plotIsobars(true);
this.plotIsotherms(true);
this.plotDryadiabats(true);
this.plotPseudoadiabats(true);
this.plotMixingratio(true);
}
/**
* @private
*/
plotIsobars(redraw) {
let min = this.coordinateSystem.getPByXY(0, this.coordinateSystem.height);
let max = this.coordinateSystem.getPByXY(0, 0);
let delta = max - min;
this._plotLines(
this.svgGroups.isobars,
this.options.isobars,
{
min: min,
max: max,
interval: (delta > 500) ? 100 : (delta > 50) ? 10 : 1
},
p => {
let y = this.coordinateSystem.getYByXP(0, p);
return [[0, y], [this.coordinateSystem.width, y]];
},
redraw
);
}
/**
* @private
*/
plotIsotherms(redraw) {
let min = tempKelvinToCelsius(
this.coordinateSystem.getTByXY(0, this.coordinateSystem.height));
let max = tempKelvinToCelsius(
this.coordinateSystem.getTByXY(this.coordinateSystem.width, 0));
let delta = max - min;
this._plotLines(
this.svgGroups.isotherms,
this.options.isotherms,
{
min: min,
max: max,
interval: (delta > 50) ? 10 : 5
},
T => {
T = tempCelsiusToKelvin(T);
let result = [[undefined, undefined], [undefined, undefined]];
if (this.coordinateSystem.isIsothermsVertical()) {
result[0][1] = 0;
result[1][1] = this.coordinateSystem.height;
result[0][0] = result[1][0] = this.coordinateSystem.getXByYT(result[0][1], T);
}
else {
result[0][1] = 0;
result[0][0] = this.coordinateSystem.getXByYT(result[0][1], T);
if (result[0][0] < 0)
result[0][1] = this.coordinateSystem.getYByXT(result[0][0] = 0, T);
result[1][0] = this.coordinateSystem.width;
result[1][1] = this.coordinateSystem.getYByXT(result[1][0], T);
if (result[1][1] === undefined) {
result[1][0] = result[0][0];
result[1][1] = this.coordinateSystem.height;
}
else if (result[1][1] > this.coordinateSystem.height) {
result[1][1] = this.coordinateSystem.height;
result[1][0] = this.coordinateSystem.getXByYT(result[1][1], T);
}
}
return result;
},
redraw
);
}
/**
* @private
*/
plotDryadiabats(redraw) {
this._plotLines(
this.svgGroups.dryadiabats,
this.options.dryadiabats,
{
min: tempKelvinToCelsius(
potentialTempByTempAndPres(
this.coordinateSystem.getTByXY(0, 0),
this.coordinateSystem.getPByXY(0, 0))),
max: tempKelvinToCelsius(
potentialTempByTempAndPres(
this.coordinateSystem.getTByXY(this.coordinateSystem.width, this.coordinateSystem.height),
this.coordinateSystem.getPByXY(this.coordinateSystem.width, this.coordinateSystem.height))),
interval: 10
},
T => {
let TKelvin = tempCelsiusToKelvin(T);
let y0 = 0;
let x0 = this.coordinateSystem.getXByYPotentialTemperature(y0, TKelvin);
if (x0 === undefined ||
x0 > this.coordinateSystem.width) {
x0 = this.coordinateSystem.width;
y0 = this.coordinateSystem.getYByXPotentialTemperature(x0, TKelvin);
}
let x1 = 0;
let y1 = this.coordinateSystem.getYByXPotentialTemperature(x1, TKelvin);
if (y1 === undefined ||
y1 > this.coordinateSystem.height) {
y1 = this.coordinateSystem.height;
x1 = this.coordinateSystem.getXByYPotentialTemperature(y1, TKelvin);
}
if (x0 === undefined ||
y0 === undefined ||
x1 === undefined ||
y1 === undefined)
return undefined;
if (this.coordinateSystem.isDryAdiabatStraightLine()) {
return [[x0, y0], [x1, y1]];
}
else {
let points = [[x0, y0]];
let yInterval = 10;
for (let y=y0+yInterval; y<y1; y+=yInterval) {
points.push([
this.coordinateSystem.getXByYPotentialTemperature(y, TKelvin),
y
]);
}
points.push([x1, y1]);
return points;
}
},
redraw
);
}
/**
* @private
*/
plotPseudoadiabats(redraw) {
this._plotLines(
this.svgGroups.pseudoadiabats,
this.options.pseudoadiabats,
{
lines: [-18, -5, 10, 30, 60, 110, 180]
},
thetae => {
let thetaeKelvin = tempCelsiusToKelvin(thetae);
const y0 =
Math.max(
0,
(this.options.pseudoadiabats.maxPressure === undefined)
? 0
: this.coordinateSystem.getYByPEquiPotTemp(
this.options.pseudoadiabats.maxPressure, thetaeKelvin)
);
const x0 = this.coordinateSystem.getXByYEquiPotTemp(y0, thetaeKelvin);
const y1 =
Math.min(
this.coordinateSystem.height,
(this.options.pseudoadiabats.minPressure === undefined)
? this.coordinateSystem.height
: this.coordinateSystem.getYByPEquiPotTemp(
this.options.pseudoadiabats.minPressure, thetaeKelvin)
);
const x1 = this.coordinateSystem.getXByYEquiPotTemp(y1, thetaeKelvin);
let points = [[x0, y0]];
let yInterval = 10;
for (let y=y0+yInterval; y<y1; y+=yInterval) {
points.push([
this.coordinateSystem.getXByYEquiPotTemp(y, thetaeKelvin),
y
]);
}
points.push([x1, y1]);
return points;
},
redraw
);
}
/**
* @private
*/
plotMixingratio(redraw) {
this._plotLines(
this.svgGroups.mixingratio,
this.options.mixingratio,
{
lines: [0.01, 0.1, 1, 2, 4, 7, 10, 16, 21, 32, 40]
},
hmr => {
const y0 =
Math.max(
0,
(this.options.mixingratio.maxPressure === undefined)
? 0
: this.coordinateSystem.getYByPHMR(
this.options.mixingratio.maxPressure, hmr)
);
const x0 = this.coordinateSystem.getXByYHMR(y0, hmr);
const y1 =
Math.min(
this.coordinateSystem.height,
(this.options.mixingratio.minPressure === undefined)
? this.coordinateSystem.height
: this.coordinateSystem.getYByPHMR(
this.options.mixingratio.minPressure, hmr)
);
const x1 = this.coordinateSystem.getXByYHMR(y1, hmr);
let points = [[x0, y0]];
const yInterval = 10;
for (let y=y0+yInterval; y<y1; y+=yInterval) {
points.push([
this.coordinateSystem.getXByYHMR(y, hmr),
y
]);
}
points.push([x1, y1]);
return points;
},
redraw
);
}
/**
* @private
*/
_plotLines(node, options, valuesOptions, pointsFunc, redraw) {
options.visible
? node.show()
: node.hide();
if (!redraw)
return;
node.clear();
let lines = [];
if (options.lines !== undefined)
lines = options.lines;
else if (options.min === undefined &&
options.max === undefined &&
options.interval === undefined &&
valuesOptions.lines !== undefined)
lines = valuesOptions.lines;
else {
if (options.min !== undefined)
valuesOptions.min = options.min;
if (options.max !== undefined)
valuesOptions.max = options.max;
let interval = options.interval;
if (interval === undefined)
interval = valuesOptions.interval;
let start = Math.ceil(valuesOptions.min/interval)*interval;
let end = Math.floor(valuesOptions.max/interval)*interval;
for (let v=start; v<=end; v+=interval) {
lines.push(v);
}
}
let highlightLineWidth = 3;
if (options.style.width !== undefined)
highlightLineWidth = options.style.width+2;
lines.forEach(function (v) {
let points = pointsFunc.call(this, v);
let line = (points.length == 2) ?
node.line(points[0][0], this.coordinateSystem.height-points[0][1],
points[1][0], this.coordinateSystem.height-points[1][1])
.stroke(options.style) :
node.polyline(points.map(function (point) {
point[1] = this.coordinateSystem.height - point[1];
return point;
}, this))
.fill('none').stroke(options.style);
if (options.highlightedLines !== undefined) {
options.highlightedLines.forEach(vHighlight => {
if (v == tempKelvinToCelsius(vHighlight))
line.stroke({width: highlightLineWidth});
});
}
}, this);
}
/**
* Extend an event with temperature and pressure.
*
* @override
*/
getExtendedEvent(e, p) {
e = super.getExtendedEvent(e, p);
e.diagramTmpk =
this.coordinateSystem.getTByXY(e.elementX,
this.coordinateSystem.height - e.elementY);
return e;
}
/**
* Initialize hover labels options.
*
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~hoverLabelsOptions}
* options - Hover labels options.
* @override
*/
_initHoverLabels({
visible = true,
type = 'mousemove',
maxDistance = undefined,
remote = true,
insertLabelsFunc = undefined,
getHoverSounding = undefined,
pres = {},
temp = {},
dewp = {},
wetbulb = {}
}) {
pres.length = ('length' in pres) ? pres.length : 60;
pres.align = ('align' in pres) ? pres.align : 'left';
if (!('visible' in pres))
pres.visible = true;
if (!('style' in pres))
pres.style = {};
pres.font = getNormalizedFontOptions(pres.font, {
anchor: (pres.align == 'right') ? 'end' : 'start'
});
if (!('fill' in pres))
pres.fill = {};
if (pres.fill.opacity === undefined)
pres.fill.opacity = 0.7;
if (pres.horizontalMargin === undefined)
pres.horizontalMargin = 5;
if (!('visible' in temp))
temp.visible = true;
if (!('style' in temp))
temp.style = {};
temp.font = getNormalizedFontOptions(temp.font, {
anchor: 'start',
'alignment-baseline': 'bottom'
});
if (!('fill' in temp))
temp.fill = {};
if (temp.fill.opacity === undefined)
temp.fill.opacity = 0.7;
temp.radius = ('radius' in temp) ? temp.radius : undefined;
temp.radiusPlus = ('radiusPlus' in temp) ? temp.radiusPlus : 2;
if (temp.horizontalMargin === undefined)
temp.horizontalMargin = 10;
if (!('visible' in dewp))
dewp.visible = true;
if (!('style' in dewp))
dewp.style = {};
dewp.font = getNormalizedFontOptions(dewp.font, {
anchor: 'end',
'alignment-baseline': 'bottom'
});
if (!('fill' in dewp))
dewp.fill = {};
if (dewp.fill.opacity === undefined)
dewp.fill.opacity = 0.7;
dewp.radius = ('radius' in dewp) ? dewp.radius : undefined;
dewp.radiusPlus = ('radiusPlus' in dewp) ? dewp.radiusPlus : 2;
if (dewp.horizontalMargin === undefined)
dewp.horizontalMargin = 10;
if (!('visible' in wetbulb))
wetbulb.visible = true;
if (!('style' in wetbulb))
wetbulb.style = {};
wetbulb.font = getNormalizedFontOptions(wetbulb.font, {
anchor: 'middle'
});
if (!('fill' in wetbulb))
wetbulb.fill = {};
if (wetbulb.fill.opacity === undefined)
wetbulb.fill.opacity = 0.7;
wetbulb.radius = ('radius' in wetbulb) ? wetbulb.radius : undefined;
wetbulb.radiusPlus = ('radiusPlus' in wetbulb) ? wetbulb.radiusPlus : 2;
if (wetbulb.verticalMargin === undefined)
wetbulb.verticalMargin = 10;
if (insertLabelsFunc === undefined)
insertLabelsFunc =
this._makeInsertLabelsFunc(pres, temp, dewp, wetbulb);
super._initHoverLabels({
visible,
type,
maxDistance,
remote,
insertLabelsFunc,
getHoverSounding,
});
}
/**
* Makes a default insertLabelsFunc.
*
* @param {Object} pres
* @param {Object} temp
* @param {Object} dewp
* @param {Object} wetbulb
* @private
*/
_makeInsertLabelsFunc(pres, temp, dewp, wetbulb) {
return (sounding, levelData, group) => {
group.clear();
if (levelData.pres === undefined)
return;
if (pres.visible)
drawPressureHoverLabelInto(group, levelData, this.coordinateSystem, pres);
this.dataGroupIds.reverse().forEach(dataGroupId => {
let labelOptions = {
visible: false
};
switch (dataGroupId) {
case 'temp': labelOptions = temp; break;
case 'dewp': labelOptions = dewp; break;
case 'wetbulb': labelOptions = wetbulb; break;
}
if (!labelOptions.visible)
return;
const { x, y, value, unit } =
this._getCoordinatesByLevelData(dataGroupId,
sounding, levelData, this);
if (x === undefined ||
y === undefined)
return;
const lineWidth =
(dataGroupId in this.hoverLabelsSounding.options.diagram)
? this.hoverLabelsSounding.options.diagram[dataGroupId].style.width
: 3;
const radius = (labelOptions.radius === undefined)
? lineWidth + labelOptions.radiusPlus
: labelOptions.radius;
const fillOptions = labelOptions.style;
if (!('color' in fillOptions) &&
(dataGroupId in this.hoverLabelsSounding.options.diagram))
fillOptions.color = sounding.options.diagram[dataGroupId].style.color;
group
.circle(2 * radius)
.attr({ cx: x, cy: y })
.fill(fillOptions);
drawTextInto({
node: group,
text: `${value} ${unit}`,
x,
y,
horizontalMargin: labelOptions.horizontalMargin,
verticalMargin: labelOptions.verticalMargin,
font: labelOptions.font,
fill: labelOptions.fill
});
});
};
}
}
export default TDDiagram;
/**
* Draws pressure hover label.
*
* @param {external:SVG} svgNode - SVG node to draw into.
* @param {number} pres - Pressure.
* @param {module:meteoJS/thermodynamicDiagram/coordinateSystem.CoordinateSystem}
* coordinateSystem - Coordinate system.
* @param {module:meteoJS/thermodynamicDiagram/tdDiagram~presLabelOptions}
* [options] - Options.
*/
export function drawPressureHoverLabelInto(svgNode, levelData, coordinateSystem, {
length = 60,
align = 'left',
horizontalMargin = undefined,
verticalMargin = undefined,
style = {},
font = {},
fill = {}
} = {}) {
let x0 = 0;
let x1 = length;
const match = /^([0-9]+)%$/.exec(x1);
if (match)
x1 = match[1] / 100 * coordinateSystem.width;
if (align == 'right') {
x0 = coordinateSystem.width;
x1 = coordinateSystem.width - x1;
}
const y = coordinateSystem.height -
coordinateSystem.getYByXP(0, levelData.pres);
style = getNormalizedLineStyleOptions(style);
svgNode
.line([
[Math.min(x0, x1), y],
[Math.max(x0, x1), y]
])
.stroke(style);
font = getNormalizedFontOptions(font);
font['alignment-baseline'] = 'bottom';
drawTextInto({
node: svgNode,
text: `${Math.round(levelData.pres)} hPa`,
x: x0,
y,
horizontalMargin,
verticalMargin,
font,
fill
});
font['alignment-baseline'] = 'top';
let hghtStr = (levelData.hght === undefined)
? `~${Math.round(altitudeISAByPres(levelData.pres))} m`
: `${Math.round(levelData.hght)} m`;
drawTextInto({
node: svgNode,
text: hghtStr,
x: x0,
y: y,
horizontalMargin,
verticalMargin,
font,
fill
});
}
function getNormalizedDiagramLineOptions({
highlightedLines = undefined,
interval = undefined,
lines = undefined,
max = undefined,
min = undefined,
maxPressure = undefined,
minPressure = undefined,
style = undefined,
visible = undefined
}, defaults = {}) {
return {
highlightedLines: getFirstDefinedValue(highlightedLines, defaults.highlightedLines),
interval: getFirstDefinedValue(interval, defaults.interval),
lines: getFirstDefinedValue(lines, defaults.lines),
max: getFirstDefinedValue(max, defaults.max),
min: getFirstDefinedValue(min, defaults.min),
maxPressure: getFirstDefinedValue(maxPressure, defaults.maxPressure),
minPressure: getFirstDefinedValue(minPressure, defaults.minPressure),
style: getNormalizedLineStyleOptions(style, defaults.style),
visible: getFirstDefinedValue(visible, defaults.visible, true)
};
}