Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 9 additions & 40 deletions bulb/src/dms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,8 @@ impl AncillaryData for DmsAnc {
let mut lines: Vec<MsgLine> =
serde_wasm_bindgen::from_value(value)?;
// NOTE: patterns *must* be populated before this!
lines.retain(|ln| {
self.has_compose_pattern(&ln.msg_pattern)
&& (ln.restrict_hashtag.is_none()
|| ln
.restrict_hashtag
.as_ref()
.is_some_and(|h| pri.has_hashtag(h)))
});
lines.retain(|ln| self.has_compose_pattern(&ln.msg_pattern));
lines.sort();
self.lines = lines;
}
Asset::Words => {
Expand Down Expand Up @@ -386,21 +380,12 @@ impl DmsAnc {
fn make_lines_html<'p>(
&self,
dms: &NtcipDms,
pat_def: &MsgPattern,
pat: &MsgPattern,
ms_cur: &str,
div: &'p mut html::Div<'p>,
) {
// NOTE: this prevents lifetime from escaping
let mut pat = pat_def;
if self.pat_lines(pat).count() == 0 {
let n_lines = MessagePattern::new(dms, &pat.multi).widths().count();
match self.find_substitute(pat, n_lines) {
Some(sub) => pat = sub,
None => return,
}
}
let widths = MessagePattern::new(dms, &pat_def.multi).widths();
let cur_lines = MessagePattern::new(dms, &pat_def.multi)
let widths = MessagePattern::new(dms, &pat.multi).widths();
let cur_lines = MessagePattern::new(dms, &pat.multi)
.lines(ms_cur)
.chain(repeat(""));
div.id("mc_lines").class("column");
Expand Down Expand Up @@ -447,31 +432,15 @@ impl DmsAnc {
div.close();
}

/// Find a substitute message pattern
fn find_substitute(
&self,
pat: &MsgPattern,
n_lines: usize,
) -> Option<&MsgPattern> {
self.compose_patterns
.iter()
.find(|&mp| mp != pat && self.max_line(mp) == n_lines)
}

/// Get max line number of a pattern
fn max_line(&self, pat: &MsgPattern) -> usize {
self.pat_lines(pat)
.map(|ml| usize::from(ml.line))
.max()
.unwrap_or_default()
}

/// Get iterator of lines in a message pattern
fn pat_lines<'a>(
&'a self,
pat: &'a MsgPattern,
) -> impl Iterator<Item = &'a MsgLine> {
self.lines.iter().filter(|ml| ml.msg_pattern == pat.name)
self.lines.iter().filter(|ml| {
ml.msg_pattern == pat.name
|| Some(&ml.msg_pattern) == pat.prototype.as_ref()
})
}

/// Get line that fits on sign
Expand Down
113 changes: 54 additions & 59 deletions bulb/src/msgpattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ use serde::Deserialize;
use std::borrow::Cow;
use std::cmp::Ordering;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
HtmlElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
};
use web_sys::{HtmlElement, HtmlSelectElement, HtmlTextAreaElement};

/// NTCIP sign
type NtcipDms = ntcip::dms::Dms<256, 24, 32>;
Expand All @@ -52,16 +50,43 @@ pub struct GraphicName {
}

/// Message Line
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
#[allow(dead_code)]
pub struct MsgLine {
pub name: String,
pub msg_pattern: String,
pub restrict_hashtag: Option<String>,
pub line: u16,
pub rank: u16,
pub multi: String,
}

impl PartialOrd for MsgLine {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl Ord for MsgLine {
fn cmp(&self, other: &Self) -> Ordering {
if self == other {
return Ordering::Equal;
}
let line_ord = self.line.cmp(&other.line);
if line_ord != Ordering::Equal {
return line_ord;
}
let rank_ord = self.rank.cmp(&other.rank);
if rank_ord != Ordering::Equal {
return rank_ord;
}
let ms_ord = self.multi.cmp(&other.multi);
if ms_ord != Ordering::Equal {
return ms_ord;
}
self.name.cmp(&other.name)
}
}

/// Ancillary message pattern data
#[derive(Default)]
pub struct MsgPatternAnc {
Expand All @@ -77,6 +102,7 @@ pub struct MsgPatternAnc {
pub struct MsgPattern {
pub name: String,
pub compose_hashtag: Option<String>,
pub prototype: Option<String>,
pub multi: String,
pub compose_cfgs: Vec<String>,
pub planned_cfgs: Vec<String>,
Expand Down Expand Up @@ -208,16 +234,22 @@ impl Ord for MsgPattern {
} else if other_combine && !self_combine {
return Ordering::Greater;
}
// prefer patterns with shorter MULTI strings
let len_ord = self.multi.len().cmp(&other.multi.len());
if len_ord != Ordering::Equal {
return len_ord;
}
let ms_ord = self.multi.cmp(&other.multi);
if ms_ord != Ordering::Equal {
ms_ord
} else {
self.name.cmp(&other.name)
return ms_ord;
}
// prefer patterns with prototypes
if self.prototype.is_some() && other.prototype.is_none() {
return Ordering::Less;
} else if self.prototype.is_none() && other.prototype.is_some() {
return Ordering::Greater;
}
self.name.cmp(&other.name)
}
}

Expand Down Expand Up @@ -367,6 +399,15 @@ impl MsgPattern {
.size(16)
.value(opt_ref(&self.compose_hashtag));
div.close();
div = page.frag::<html::Div>();
div.class("row");
div.label().r#for("mp_prototype").cdata("Prototype").close();
div.input()
.id("mp_prototype")
.maxlength(20)
.size(20)
.value(opt_ref(&self.prototype));
div.close();
let mut fs = page.frag::<html::FieldSet>();
let mut legend = fs.legend();
legend
Expand Down Expand Up @@ -414,16 +455,7 @@ impl MsgPattern {
div.close();
div = fs.div();
div.id("mp_lines_div").class("no-display");
let mut div2 = div.div();
div2.class("row");
div2.label()
.r#for("mp_filter")
.cdata("Filter Restrict")
.close();
div2.input().id("mp_filter").r#type("checkbox");
div2.input().id("mp_restrict").maxlength(16).size(16);
div2.close();
self.render_lines(anc, None, &mut div.div());
self.render_lines(anc, &mut div.div());
div.close();
fs.close();
div = page.frag::<html::Div>();
Expand Down Expand Up @@ -523,62 +555,26 @@ impl MsgPattern {
fn render_lines<'p>(
&self,
anc: &MsgPatternAnc,
filter: Option<&str>,
div: &'p mut html::Div<'p>,
) {
div.id("mp_lines").class("scroll_table");
div.class("scroll_table");
let mut table = div.table();
let mut thead = table.thead();
let mut tr = thead.tr();
tr.th().cdata("Ln").close();
tr.th().cdata("Rank").close();
tr.th().cdata("MULTI").close();
if filter.is_some() {
tr.th().cdata("Restrict").close();
}
thead.close();
for ln in &anc.lines {
match (&filter, &ln.restrict_hashtag) {
(Some(filter), Some(restrict)) => {
if !restrict.to_lowercase().contains(filter) {
continue;
}
}
(Some(_filter), None) => continue,
(None, Some(_restrict)) => continue,
(None, None) => (),
}
let mut tr = table.tr();
tr.td().cdata(ln.line).close();
tr.td().cdata(ln.rank).close();
tr.td().cdata(&ln.multi).close();
if filter.is_some() {
tr.td().cdata(opt_ref(&ln.restrict_hashtag)).close();
}
tr.close();
}
table.close();
div.close();
}

/// Replace message lines
fn replace_lines(&self, anc: &MsgPatternAnc) {
let doc = Doc::get();
if let Some(mp_filter) = doc.try_elem::<HtmlInputElement>("mp_filter") {
let restrict; // String
let mut filter = None;
if mp_filter.checked()
&& let Some(mp_restrict) =
doc.try_elem::<HtmlInputElement>("mp_restrict")
{
restrict = mp_restrict.value().to_lowercase();
filter = Some(restrict.as_str());
}
let mut page = Page::new();
let mut div = page.frag::<html::Div>();
self.render_lines(anc, filter, &mut div);
let mp_lines = doc.elem::<HtmlElement>("mp_lines");
mp_lines.set_outer_html(&String::from(page));
}
}
}

impl Card for MsgPattern {
Expand Down Expand Up @@ -631,6 +627,7 @@ impl Card for MsgPattern {
let mut fields = Fields::new();
fields.changed_text_area("multi", &self.multi);
fields.changed_input("compose_hashtag", &self.compose_hashtag);
fields.changed_input("mp_prototype", &self.prototype);
fields.changed_input("flash_beacon", self.flash_beacon);
fields.changed_input("pixel_service", self.pixel_service);
fields.into_value().to_string()
Expand All @@ -641,8 +638,6 @@ impl Card for MsgPattern {
let doc = Doc::get();
if id == "mp_config" {
self.replace_preview(&anc);
} else if id == "mp_filter" || id == "mp_restrict" {
self.replace_lines(&anc);
} else if let Ok(tab) = Tab::try_from(id.as_str()) {
if let Tab::Preview = tab {
self.replace_preview(&anc);
Expand Down
36 changes: 19 additions & 17 deletions docs/message_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ with a sign config. They can be:
[hashtag]
* Scheduled by [device actions] as part of an [action plan]

Setting a **prototype** [extends](#message-lines) another pattern. Both
patterns should have the same configuration of
[fillable text rectangles](#fillable-text-rectangles).

If **flash beacon** is selected, any associated [beacon] (_internal_ or
_external_) will also be activated.

Expand All @@ -23,12 +27,12 @@ active for long periods of time.
* `iris/api/msg_pattern` (primary)
* `iris/api/msg_pattern/{name}`

| Access | Primary | Secondary |
|--------------|-------------------------|-------------------------------|
| 👁️ View | name, compose\_cfgs, planned\_cfgs | |
| 👉 Operate | | |
| 💡 Manage | compose\_hashtag, multi | flash\_beacon, pixel\_service |
| 🔧 Configure | | |
| Access | Primary | Secondary |
|--------------|------------------------------------|-----------|
| 👁️ View | name, compose\_cfgs, planned\_cfgs | |
| 👉 Operate | | |
| 💡 Manage | compose\_hashtag, prototype, multi | flash\_beacon, pixel\_service |
| 🔧 Configure | | |

</details>

Expand All @@ -51,25 +55,23 @@ be `[tr1,1,100,16]CRASH[nl]AHEAD[g5]`.

A pattern with fillable text rectangles can have lines of text associated with
it. Each line is used in a specific fillable rectangle of the pattern. Lines
can be ordered in the message composer by **rank**, 1-99. Lines can also be
restricted to specific signs by adding a **restrict** [hashtag].
can be ordered in the message composer by **rank**, 1-99.

If a pattern has fillable text rectangles but no lines, a **substitute**
pattern will be chosen to provide them instead. Both patterns must have the
same number of lines in their text rectangles.
All lines from a **prototype** pattern will also be included, allowing a
pattern to be extended for specific _compose_ [hashtag]s.

<details>
<summary>API Resources 🕵️ </summary>

* `iris/api/msg_line` (primary)
* `iris/api/msg_line/{name}`

| Access | Primary | Secondary |
|--------------|--------------------------------|-----------|
| 👁️ View | name, msg\_pattern | |
| 👉 Operate | | |
| 💡 Manage | line, multi, restrict\_hashtag | rank |
| 🔧 Configure | | |
| Access | Primary | Secondary |
|--------------|--------------------|-----------|
| 👁️ View | name, msg\_pattern | |
| 👉 Operate | | |
| 💡 Manage | line, rank, multi | |
| 🔧 Configure | | |

</details>

Expand Down
1 change: 1 addition & 0 deletions etc/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ msg.pattern=Message Pattern
msg.patterns=Message Patterns
msg.pattern.name=Name
msg.pattern.compose.hashtag=Compose #Tag
msg.pattern.prototype=Prototype Pattern
msg.pattern.config=Sign Config
msg.pattern.multi=MULTI String
msg.pattern.unknown.hint=Unknown Pattern: please specify an existing Message Pattern
Expand Down
2 changes: 1 addition & 1 deletion honeybee/src/honey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ const fn one_sql(res: Res) -> Option<&'static str> {
LcsState => query::LCS_STATE_ONE,
Modem => query::MODEM_ONE,
MonitorStyle => query::MONITOR_STYLE_ONE,
MsgLine => query::MSG_LINE_ONE,
MsgLine => return None, /* NOTE: all request only */
MsgPattern => query::MSG_PATTERN_ONE,
Permission => query::PERMISSION_ONE,
PlanPhase => query::PLAN_PHASE_ONE,
Expand Down
Loading