use super::{xml_escape, xml_str, xml_u32};
use crate::error::OnvifError;
use crate::soap::{SoapError, XmlNode};
#[derive(Debug, Clone, Default)]
pub struct PtzSpaceRange {
pub uri: String,
pub x_range: (f32, f32),
pub y_range: Option<(f32, f32)>,
}
fn parse_space_range(node: &XmlNode) -> PtzSpaceRange {
let uri = xml_str(node, "URI").unwrap_or_default();
let x_range = node
.child("XRange")
.map(|r| {
let min = r
.child("Min")
.and_then(|n| n.text().parse().ok())
.unwrap_or(-1.0);
let max = r
.child("Max")
.and_then(|n| n.text().parse().ok())
.unwrap_or(1.0);
(min, max)
})
.unwrap_or((-1.0, 1.0));
let y_range = node.child("YRange").map(|r| {
let min = r
.child("Min")
.and_then(|n| n.text().parse().ok())
.unwrap_or(-1.0);
let max = r
.child("Max")
.and_then(|n| n.text().parse().ok())
.unwrap_or(1.0);
(min, max)
});
PtzSpaceRange {
uri,
x_range,
y_range,
}
}
#[derive(Debug, Clone, Default)]
pub struct PtzSpeed {
pub pan_tilt: Option<(f32, f32)>,
pub zoom: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct PtzConfiguration {
pub token: String,
pub name: String,
pub use_count: u32,
pub node_token: String,
pub default_ptz_timeout: Option<String>,
pub default_abs_pan_tilt_space: Option<String>,
pub default_abs_zoom_space: Option<String>,
pub default_rel_pan_tilt_space: Option<String>,
pub default_rel_zoom_space: Option<String>,
pub default_cont_pan_tilt_space: Option<String>,
pub default_cont_zoom_space: Option<String>,
pub default_ptz_speed: Option<PtzSpeed>,
pub pan_tilt_limits: Option<PtzSpaceRange>,
pub zoom_limits: Option<PtzSpaceRange>,
}
impl PtzConfiguration {
pub(crate) fn from_xml(node: &XmlNode) -> Result<Self, OnvifError> {
let token = node
.attr("token")
.filter(|t| !t.is_empty())
.ok_or_else(|| SoapError::missing("PTZConfiguration/@token"))?
.to_string();
Ok(Self {
token,
name: xml_str(node, "Name").unwrap_or_default(),
use_count: xml_u32(node, "UseCount").unwrap_or(0),
node_token: xml_str(node, "NodeToken").unwrap_or_default(),
default_ptz_timeout: xml_str(node, "DefaultPTZTimeout"),
default_abs_pan_tilt_space: xml_str(node, "DefaultAbsolutePanTiltPositionSpace"),
default_abs_zoom_space: xml_str(node, "DefaultAbsoluteZoomPositionSpace"),
default_rel_pan_tilt_space: xml_str(node, "DefaultRelativePanTiltTranslationSpace"),
default_rel_zoom_space: xml_str(node, "DefaultRelativeZoomTranslationSpace"),
default_cont_pan_tilt_space: xml_str(node, "DefaultContinuousPanTiltVelocitySpace"),
default_cont_zoom_space: xml_str(node, "DefaultContinuousZoomVelocitySpace"),
default_ptz_speed: node.child("DefaultPTZSpeed").map(|s| PtzSpeed {
pan_tilt: s.child("PanTilt").and_then(|n| {
let x = n.attr("x")?.parse().ok()?;
let y = n.attr("y")?.parse().ok()?;
Some((x, y))
}),
zoom: s.child("Zoom").and_then(|n| n.attr("x")?.parse().ok()),
}),
pan_tilt_limits: node
.path(&["PanTiltLimits", "Range"])
.map(parse_space_range),
zoom_limits: node.path(&["ZoomLimits", "Range"]).map(parse_space_range),
})
}
pub(crate) fn vec_from_xml(resp: &XmlNode) -> Result<Vec<Self>, OnvifError> {
resp.children_named("PTZConfiguration")
.map(Self::from_xml)
.collect()
}
pub(crate) fn to_xml_body(&self) -> String {
let opt_str = |v: &Option<String>, tag: &str| -> String {
v.as_deref()
.map(|s| format!("<tt:{tag}>{}<tt:/{tag}>", xml_escape(s)))
.unwrap_or_default()
};
let timeout_el = opt_str(&self.default_ptz_timeout, "DefaultPTZTimeout");
let abs_pt = opt_str(
&self.default_abs_pan_tilt_space,
"DefaultAbsolutePanTiltPositionSpace",
);
let abs_z = opt_str(
&self.default_abs_zoom_space,
"DefaultAbsoluteZoomPositionSpace",
);
let rel_pt = opt_str(
&self.default_rel_pan_tilt_space,
"DefaultRelativePanTiltTranslationSpace",
);
let rel_z = opt_str(
&self.default_rel_zoom_space,
"DefaultRelativeZoomTranslationSpace",
);
let cont_pt = opt_str(
&self.default_cont_pan_tilt_space,
"DefaultContinuousPanTiltVelocitySpace",
);
let cont_z = opt_str(
&self.default_cont_zoom_space,
"DefaultContinuousZoomVelocitySpace",
);
let speed_el = match &self.default_ptz_speed {
Some(s) => {
let pt = s
.pan_tilt
.map(|(x, y)| format!("<tt:PanTilt x=\"{x}\" y=\"{y}\"/>"))
.unwrap_or_default();
let z = s
.zoom
.map(|x| format!("<tt:Zoom x=\"{x}\"/>"))
.unwrap_or_default();
format!("<tt:DefaultPTZSpeed>{pt}{z}</tt:DefaultPTZSpeed>")
}
None => String::new(),
};
format!(
"<tptz:PTZConfiguration token=\"{token}\">\
<tt:Name>{name}</tt:Name>\
<tt:UseCount>{use_count}</tt:UseCount>\
<tt:NodeToken>{node_token}</tt:NodeToken>\
{abs_pt}{abs_z}{rel_pt}{rel_z}{cont_pt}{cont_z}\
{speed_el}{timeout_el}\
</tptz:PTZConfiguration>",
token = xml_escape(&self.token),
name = xml_escape(&self.name),
use_count = self.use_count,
node_token = xml_escape(&self.node_token),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct PtzConfigurationOptions {
pub ptz_timeout_min: Option<String>,
pub ptz_timeout_max: Option<String>,
}
impl PtzConfigurationOptions {
pub(crate) fn from_xml(resp: &XmlNode) -> Result<Self, OnvifError> {
let opts = resp
.child("PTZConfigurationOptions")
.ok_or_else(|| SoapError::missing("PTZConfigurationOptions"))?;
Ok(Self {
ptz_timeout_min: opts
.path(&["PTZTimeout", "Min"])
.map(|n| n.text().to_string()),
ptz_timeout_max: opts
.path(&["PTZTimeout", "Max"])
.map(|n| n.text().to_string()),
})
}
}
#[derive(Debug, Clone)]
pub struct PtzNode {
pub token: String,
pub name: String,
pub fixed_home_position: bool,
pub home_supported: bool,
pub max_presets: u32,
pub aux_commands: Vec<String>,
pub pan_tilt_spaces: Vec<PtzSpaceRange>,
pub zoom_spaces: Vec<PtzSpaceRange>,
}
impl PtzNode {
pub(crate) fn from_xml(n: &XmlNode) -> Result<Self, OnvifError> {
let token = n
.attr("token")
.filter(|t| !t.is_empty())
.ok_or_else(|| SoapError::missing("PTZNode/@token"))?
.to_string();
let spaces = n.child("SupportedPTZSpaces");
let pan_tilt_spaces = spaces
.map(|s| {
s.children_named("AbsolutePanTiltPositionSpace")
.chain(s.children_named("RelativePanTiltTranslationSpace"))
.chain(s.children_named("ContinuousPanTiltVelocitySpace"))
.chain(s.children_named("PanTiltSpeedSpace"))
.map(parse_space_range)
.collect()
})
.unwrap_or_default();
let zoom_spaces = spaces
.map(|s| {
s.children_named("AbsoluteZoomPositionSpace")
.chain(s.children_named("RelativeZoomTranslationSpace"))
.chain(s.children_named("ContinuousZoomVelocitySpace"))
.chain(s.children_named("ZoomSpeedSpace"))
.map(parse_space_range)
.collect()
})
.unwrap_or_default();
Ok(Self {
token,
name: xml_str(n, "Name").unwrap_or_default(),
fixed_home_position: n.attr("FixedHomePosition") == Some("true"),
home_supported: n
.child("HomeSupported")
.is_some_and(|h| h.text() == "true" || h.text() == "1"),
max_presets: xml_u32(n, "MaximumNumberOfPresets").unwrap_or(0),
aux_commands: n
.children_named("AuxiliaryCommands")
.map(|c| c.text().to_string())
.collect(),
pan_tilt_spaces,
zoom_spaces,
})
}
pub(crate) fn vec_from_xml(resp: &XmlNode) -> Result<Vec<Self>, OnvifError> {
resp.children_named("PTZNode").map(Self::from_xml).collect()
}
}