diff --git a/bulb/src/dms.rs b/bulb/src/dms.rs index a03cbcfbc..11ccae5c8 100644 --- a/bulb/src/dms.rs +++ b/bulb/src/dms.rs @@ -279,14 +279,8 @@ impl AncillaryData for DmsAnc { let mut lines: Vec = 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 => { @@ -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"); @@ -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 { - 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 diff --git a/bulb/src/msgpattern.rs b/bulb/src/msgpattern.rs index 62029874e..9c3e05928 100644 --- a/bulb/src/msgpattern.rs +++ b/bulb/src/msgpattern.rs @@ -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>; @@ -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, pub line: u16, + pub rank: u16, pub multi: String, } +impl PartialOrd for MsgLine { + fn partial_cmp(&self, other: &Self) -> Option { + 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 { @@ -77,6 +102,7 @@ pub struct MsgPatternAnc { pub struct MsgPattern { pub name: String, pub compose_hashtag: Option, + pub prototype: Option, pub multi: String, pub compose_cfgs: Vec, pub planned_cfgs: Vec, @@ -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) } } @@ -367,6 +399,15 @@ impl MsgPattern { .size(16) .value(opt_ref(&self.compose_hashtag)); div.close(); + div = page.frag::(); + 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::(); let mut legend = fs.legend(); legend @@ -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::(); @@ -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::("mp_filter") { - let restrict; // String - let mut filter = None; - if mp_filter.checked() - && let Some(mp_restrict) = - doc.try_elem::("mp_restrict") - { - restrict = mp_restrict.value().to_lowercase(); - filter = Some(restrict.as_str()); - } - let mut page = Page::new(); - let mut div = page.frag::(); - self.render_lines(anc, filter, &mut div); - let mp_lines = doc.elem::("mp_lines"); - mp_lines.set_outer_html(&String::from(page)); - } - } } impl Card for MsgPattern { @@ -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() @@ -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); diff --git a/docs/message_patterns.md b/docs/message_patterns.md index fa72fae28..5a001e3fd 100644 --- a/docs/message_patterns.md +++ b/docs/message_patterns.md @@ -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. @@ -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 | | | @@ -51,12 +55,10 @@ 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.
API Resources 🕵️ @@ -64,12 +66,12 @@ same number of lines in their text rectangles. * `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 | | |
diff --git a/etc/i18n/messages_en.properties b/etc/i18n/messages_en.properties index 53040fff5..f9063bc14 100644 --- a/etc/i18n/messages_en.properties +++ b/etc/i18n/messages_en.properties @@ -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 diff --git a/honeybee/src/honey.rs b/honeybee/src/honey.rs index 167314d5b..0113d0392 100644 --- a/honeybee/src/honey.rs +++ b/honeybee/src/honey.rs @@ -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, diff --git a/honeybee/src/query.rs b/honeybee/src/query.rs index a14a3f438..9bcfc3331 100644 --- a/honeybee/src/query.rs +++ b/honeybee/src/query.rs @@ -623,19 +623,13 @@ pub const MONITOR_STYLE_ONE: &str = "\ /// SQL query for all message lines (primary) pub const MSG_LINE_ALL: &str = "\ - SELECT name, msg_pattern, line, multi, restrict_hashtag \ + SELECT name, msg_pattern, line, rank, multi \ FROM iris.msg_line \ - ORDER BY msg_pattern, line, rank, multi, restrict_hashtag"; - -/// SQL query for one message line (secondary) -pub const MSG_LINE_ONE: &str = "\ - SELECT name, msg_pattern, line, multi, rank, restrict_hashtag \ - FROM iris.msg_line \ - WHERE name = $1"; + ORDER BY msg_pattern, line, rank, multi"; /// SQL query for all message patterns (primary) pub const MSG_PATTERN_ALL: &str = "\ - SELECT mp.name, multi, compose_hashtag, \ + SELECT mp.name, compose_hashtag, prototype, multi, \ to_jsonb(array_remove(array_agg(DISTINCT cd.sign_config), null)) \ AS compose_cfgs, \ to_jsonb(array_remove(array_agg(DISTINCT pd.sign_config), null)) \ @@ -652,7 +646,8 @@ pub const MSG_PATTERN_ALL: &str = "\ /// SQL query for one message pattern (secondary) pub const MSG_PATTERN_ONE: &str = "\ - SELECT mp.name, multi, flash_beacon, pixel_service, compose_hashtag, \ + SELECT mp.name, compose_hashtag, prototype, multi, flash_beacon, \ + pixel_service, \ to_jsonb(array_remove(array_agg(DISTINCT cd.sign_config), null)) \ AS compose_cfgs, \ to_jsonb(array_remove(array_agg(DISTINCT pd.sign_config), null)) \ diff --git a/sql/migrate-pat-prototype.sql b/sql/migrate-pat-prototype.sql new file mode 100644 index 000000000..a64ce6186 --- /dev/null +++ b/sql/migrate-pat-prototype.sql @@ -0,0 +1,44 @@ +\set ON_ERROR_STOP + +SET SESSION AUTHORIZATION 'tms'; +BEGIN; + +-- Add prototype to msg_pattern +DROP VIEW msg_pattern_view; +DROP VIEW msg_line_view; + +ALTER TABLE iris.msg_pattern + ADD COLUMN prototype VARCHAR(20) REFERENCES iris.msg_pattern; + +INSERT INTO iris.msg_pattern ( + name, prototype, multi, flash_beacon, pixel_service, compose_hashtag +) ( + SELECT DISTINCT + (msg_pattern || '.' || ltrim(restrict_hashtag, '#'))::VARCHAR(20), + msg_pattern, + p.multi, + flash_beacon, + pixel_service, + restrict_hashtag + FROM iris.msg_line + JOIN iris.msg_pattern p ON msg_pattern = p.name + WHERE restrict_hashtag IS NOT NULL +); + +UPDATE iris.msg_line + SET msg_pattern = (msg_pattern || '.' || ltrim(restrict_hashtag, '#'))::VARCHAR(20) + WHERE restrict_hashtag IS NOT NULL; + +ALTER TABLE iris.msg_line DROP COLUMN restrict_hashtag; + +CREATE VIEW msg_pattern_view AS + SELECT name, compose_hashtag, prototype, multi, flash_beacon, pixel_service + FROM iris.msg_pattern; +GRANT SELECT ON msg_pattern_view TO PUBLIC; + +CREATE VIEW msg_line_view AS + SELECT name, msg_pattern, line, rank, multi + FROM iris.msg_line; +GRANT SELECT ON msg_line_view TO PUBLIC; + +COMMIT; diff --git a/sql/tms-template.sql b/sql/tms-template.sql index fff3fd751..98f343d8d 100644 --- a/sql/tms-template.sql +++ b/sql/tms-template.sql @@ -3302,86 +3302,85 @@ CREATE TRIGGER dms_hashtag_trig CREATE TABLE iris.msg_pattern ( name VARCHAR(20) PRIMARY KEY, + compose_hashtag VARCHAR(16), + prototype VARCHAR(20) REFERENCES iris.msg_pattern, multi VARCHAR(1024) NOT NULL, flash_beacon BOOLEAN NOT NULL, pixel_service BOOLEAN NOT NULL, - compose_hashtag VARCHAR(16), CONSTRAINT hashtag_ck CHECK (compose_hashtag ~ '^#[A-Za-z0-9]+$') ); -INSERT INTO iris.msg_pattern (name, multi, flash_beacon, pixel_service, - compose_hashtag) -VALUES - ('.1_LINE', '', false, false, '#OneLine'), - ('.2_LINE', '[np]', false, false, '#TwoLine'), - ('.3_LINE', '', false, false, '#ThreeLine'), - ('.4_LINE', '', false, false, '#FourLine'), - ('.2_PAGE', '[np]', false, false, '#Small'), +INSERT INTO iris.msg_pattern ( + name, compose_hashtag, multi, flash_beacon, pixel_service +) VALUES + ('.1_LINE', '#OneLine', '', false, false), + ('.2_LINE', '#TwoLine', '[np]', false, false), + ('.3_LINE', '#ThreeLine', '', false, false), + ('.4_LINE', '#FourLine', '', false, false), + ('.2_PAGE', '#Small', '[np]', false, false), ('RWIS_slippery_1', + NULL, '[rwis_slippery,1]SLIPPERY[nl]ROAD[nl]DETECTED[np]USE[nl]CAUTION', false, - false, - NULL), + false), ('RWIS_slippery_2', + NULL, '[rwis_slippery,2]SLIPPERY[nl]ROAD[nl]DETECTED[np]REDUCE[nl]SPEED', false, - false, - NULL), + false), ('RWIS_slippery_3', + NULL, '[rwis_slippery,3]ICE[nl]DETECTED[np]REDUCE[nl]SPEED', false, - false, - NULL), + false), ('RWIS_windy_1', + NULL, '[rwis_windy,1]WIND GST[nl]>40 MPH[nl]DETECTED[np]USE[nl]CAUTION', false, - false, - NULL), + false), ('RWIS_windy_2', + NULL, '[rwis_windy,2]WIND GST[nl]>60 MPH[nl]DETECTED[np]REDUCE[nl]SPEED', false, - false, - NULL), + false), ('RWIS_visibility_1', + NULL, '[rwis_visibility,1]REDUCED[nl]VISBLITY[nl]DETECTED[np]USE[nl]CAUTION', false, - false, - NULL), + false), ('RWIS_visibility_2', + NULL, '[rwis_visibility,2]LOW[nl]VISBLITY[nl]DETECTED[np]REDUCE[nl]SPEED', false, - false, - NULL), + false), ('RWIS_flooding_1', + NULL, '[rwis_flooding,1]FLOODING[nl]POSSIBLE[np]USE[nl]CAUTION', false, - false, - NULL), + false), ('RWIS_flooding_2', + NULL, '[rwis_flooding,2]FLASH[nl]FLOODING[np]USE[nl]CAUTION', false, - false, - NULL); + false); CREATE TRIGGER msg_pattern_notify_trig AFTER INSERT OR UPDATE OR DELETE ON iris.msg_pattern FOR EACH STATEMENT EXECUTE FUNCTION iris.table_notify(); CREATE VIEW msg_pattern_view AS - SELECT name, multi, flash_beacon, pixel_service, compose_hashtag + SELECT name, compose_hashtag, prototype, multi, flash_beacon, pixel_service FROM iris.msg_pattern; GRANT SELECT ON msg_pattern_view TO PUBLIC; CREATE TABLE iris.msg_line ( name VARCHAR(10) PRIMARY KEY, msg_pattern VARCHAR(20) NOT NULL REFERENCES iris.msg_pattern, - restrict_hashtag VARCHAR(16), line SMALLINT NOT NULL, multi VARCHAR(64) NOT NULL, rank SMALLINT NOT NULL, - CONSTRAINT hashtag_ck CHECK (restrict_hashtag ~ '^#[A-Za-z0-9]+$'), CONSTRAINT msg_line_line CHECK ((line >= 1) AND (line <= 12)), CONSTRAINT msg_line_rank CHECK ((rank >= 1) AND (rank <= 99)) ); @@ -3391,7 +3390,7 @@ CREATE TRIGGER msg_line_notify_trig FOR EACH STATEMENT EXECUTE FUNCTION iris.table_notify(); CREATE VIEW msg_line_view AS - SELECT name, msg_pattern, restrict_hashtag, line, multi, rank + SELECT name, msg_pattern, line, rank, multi FROM iris.msg_line; GRANT SELECT ON msg_line_view TO PUBLIC; diff --git a/src/us/mn/state/dot/tms/MsgLine.java b/src/us/mn/state/dot/tms/MsgLine.java index 6ef0cc795..2bd6c5790 100644 --- a/src/us/mn/state/dot/tms/MsgLine.java +++ b/src/us/mn/state/dot/tms/MsgLine.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2004-2024 Minnesota Department of Transportation + * Copyright (C) 2004-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,12 +42,6 @@ default String getTypeName() { /** Get the message pattern */ MsgPattern getMsgPattern(); - /** Set restrict hashtag, or null for none */ - void setRestrictHashtag(String rht); - - /** Get restrict hashtag, or null for none */ - String getRestrictHashtag(); - /** Set the line number */ void setLine(short l); diff --git a/src/us/mn/state/dot/tms/MsgLineHelper.java b/src/us/mn/state/dot/tms/MsgLineHelper.java index bd0b4c670..fdc39d827 100644 --- a/src/us/mn/state/dot/tms/MsgLineHelper.java +++ b/src/us/mn/state/dot/tms/MsgLineHelper.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2011-2024 Minnesota Department of Transportation + * Copyright (C) 2011-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,25 +50,19 @@ static public boolean isMultiValid(String m) { m.equals(new MultiString(m).normalizeLine().toString()); } - /** Find all lines for a message pattern */ + /** Find all lines for a message pattern, including prototype */ static public List findAllLines(MsgPattern pat, DMS dms) { ArrayList lines = new ArrayList(); List line_rects = MsgPatternHelper.lineTextRects(pat, dms); if (line_rects == null || line_rects.size() <= 1) return lines; - if (MsgPatternHelper.lineCount(pat) == 0) { - int n_lines = line_rects.size() - 1; - pat = MsgPatternHelper.findSubstitute(pat, dms, - n_lines); - if (pat == null) - return lines; - } - Hashtags hashtags = new Hashtags(dms.getNotes()); + String prototype = pat.getPrototype(); Iterator it = iterator(); while (it.hasNext()) { MsgLine ml = it.next(); - if (checkLine(ml, pat, hashtags)) { + MsgPattern mp = ml.getMsgPattern(); + if (mp == pat || mp.getName().equals(prototype)) { MsgLine aml = abbreviateLine(ml, line_rects); if (aml != null) lines.add(aml); @@ -77,17 +71,6 @@ static public List findAllLines(MsgPattern pat, DMS dms) { return lines; } - /** Check if a message line belongs */ - static private boolean checkLine(MsgLine ml, MsgPattern pat, - Hashtags hashtags) - { - if (ml.getMsgPattern() == pat) { - String rht = ml.getRestrictHashtag(); - return (rht == null) || hashtags.contains(rht); - } - return false; - } - /** Abbreviate a line for available text rectangle */ static private MsgLine abbreviateLine(MsgLine ml, List line_rects) diff --git a/src/us/mn/state/dot/tms/MsgPattern.java b/src/us/mn/state/dot/tms/MsgPattern.java index 5e27ee935..2bfb3133d 100644 --- a/src/us/mn/state/dot/tms/MsgPattern.java +++ b/src/us/mn/state/dot/tms/MsgPattern.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2009-2025 Minnesota Department of Transportation + * Copyright (C) 2009-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,6 +36,14 @@ default String getTypeName() { /** SONAR base type name */ String SONAR_BASE = DMS.SONAR_TYPE; + /** Get prototype pattern to derive from. + * @return Prototype pattern name. */ + String getPrototype(); + + /** Set prototype pattern to derive from. + @param prototype Name of prototype pattern. */ + void setPrototype(String prototype); + /** Get the message MULTI string. * @return Message text in MULTI markup. * @see us.mn.state.dot.tms.utils.MultiString */ diff --git a/src/us/mn/state/dot/tms/MsgPatternHelper.java b/src/us/mn/state/dot/tms/MsgPatternHelper.java index a3f4c22e2..73f00a927 100644 --- a/src/us/mn/state/dot/tms/MsgPatternHelper.java +++ b/src/us/mn/state/dot/tms/MsgPatternHelper.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2009-2025 Minnesota Department of Transportation + * Copyright (C) 2009-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -141,18 +141,6 @@ static public Set findSignConfigs(MsgPattern pat) { return cfgs; } - /** Get the count of lines associated with a message pattern */ - static public int lineCount(MsgPattern pat) { - int n_lines = 0; - Iterator it = MsgLineHelper.iterator(); - while (it.hasNext()) { - MsgLine ml = it.next(); - if (ml.getMsgPattern() == pat) - n_lines = Math.max(n_lines, ml.getLine()); - } - return n_lines; - } - /** Get full text rectangle for a sign */ static private TextRect fullTextRect(DMS dms) { return (dms != null) @@ -246,23 +234,4 @@ static public List lineTextRects(MsgPattern pat, DMS dms) { } return rects; } - - /** Find a substitute pattern with associated message lines */ - static public MsgPattern findSubstitute(MsgPattern pat, DMS dms, - int n_lines) - { - Hashtags tags = new Hashtags(dms.getNotes()); - Iterator it = iterator(); - while (it.hasNext()) { - MsgPattern mp = it.next(); - if (mp != pat && isValidMulti(mp)) { - if (lineCount(mp) == n_lines) { - String ht = mp.getComposeHashtag(); - if (tags.contains(ht)) - return mp; - } - } - } - return null; - } } diff --git a/src/us/mn/state/dot/tms/TransMsgLine.java b/src/us/mn/state/dot/tms/TransMsgLine.java index f930583c2..f716c7eae 100644 --- a/src/us/mn/state/dot/tms/TransMsgLine.java +++ b/src/us/mn/state/dot/tms/TransMsgLine.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2008-2024 Minnesota Department of Transportation + * Copyright (C) 2008-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,18 +70,6 @@ public MsgPattern getMsgPattern() { return null; } - /** Set restrict hashtag, or null for none */ - @Override - public void setRestrictHashtag(String rht) { - // do nothing - } - - /** Get restrict hashtag, or null for none */ - @Override - public String getRestrictHashtag() { - return null; - } - /** Set the line number */ @Override public void setLine(short l) { diff --git a/src/us/mn/state/dot/tms/client/dms/MsgLineComparator.java b/src/us/mn/state/dot/tms/client/dms/MsgLineComparator.java index 4886f1eae..0b2d6b297 100644 --- a/src/us/mn/state/dot/tms/client/dms/MsgLineComparator.java +++ b/src/us/mn/state/dot/tms/client/dms/MsgLineComparator.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2008-2023 Minnesota Department of Transportation + * Copyright (C) 2008-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,8 +28,6 @@ public class MsgLineComparator implements Comparator { @Override public int compare(MsgLine ml0, MsgLine ml1) { int c = compareLine(ml0, ml1); - if (c == 0) - c = compareRestrictHashtag(ml0, ml1); if (c == 0) c = compareRank(ml0, ml1); if (c == 0) @@ -39,19 +37,6 @@ public int compare(MsgLine ml0, MsgLine ml1) { return c; } - /** Compare hashtags */ - private int compareRestrictHashtag(MsgLine ml0, MsgLine ml1) { - String ht0 = ml0.getRestrictHashtag(); - String ht1 = ml1.getRestrictHashtag(); - if (ht0 == ht1) - return 0; - if (ht0 == null) - return -1; - if (ht1 == null) - return 1; - return ht0.compareTo(ht1); - } - /** Compare line numbers */ private int compareLine(MsgLine ml0, MsgLine ml1) { Short l0 = ml0.getLine(); diff --git a/src/us/mn/state/dot/tms/client/dms/MsgLineTableModel.java b/src/us/mn/state/dot/tms/client/dms/MsgLineTableModel.java index 0ebd77e4d..0ac4d0935 100644 --- a/src/us/mn/state/dot/tms/client/dms/MsgLineTableModel.java +++ b/src/us/mn/state/dot/tms/client/dms/MsgLineTableModel.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2005-2024 Minnesota Department of Transportation + * Copyright (C) 2005-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; -import us.mn.state.dot.tms.Hashtags; import us.mn.state.dot.tms.MsgPattern; import us.mn.state.dot.tms.MsgLine; import us.mn.state.dot.tms.client.Session; @@ -64,23 +63,26 @@ static private String formatMulti(Object value) { @Override protected ArrayList> createColumns() { ArrayList> cols = - new ArrayList>(4); - cols.add(new ProxyColumn("hashtag", 72) { + new ArrayList>(3); + cols.add(new ProxyColumn("dms.line", 36, Short.class){ public Object getValueAt(MsgLine ml) { - return ml.getRestrictHashtag(); + return ml.getLine(); } public boolean isEditable(MsgLine ml) { return canWrite(ml); } public void setValueAt(MsgLine ml, Object value) { - selected = ml.getName(); - String ht = Hashtags.normalize(value.toString()); - ml.setRestrictHashtag(ht); + if (value instanceof Number) { + selected = ml.getName(); + Number n = (Number) value; + ml.setLine(n.shortValue()); + } } }); - cols.add(new ProxyColumn("dms.line", 36, Short.class){ + cols.add(new ProxyColumn("dms.rank", 40, Short.class) + { public Object getValueAt(MsgLine ml) { - return ml.getLine(); + return ml.getRank(); } public boolean isEditable(MsgLine ml) { return canWrite(ml); @@ -89,9 +91,12 @@ public void setValueAt(MsgLine ml, Object value) { if (value instanceof Number) { selected = ml.getName(); Number n = (Number) value; - ml.setLine(n.shortValue()); + ml.setRank(n.shortValue()); } } + protected TableCellEditor createCellEditor() { + return new RankCellEditor(); + } }); cols.add(new ProxyColumn("dms.multi", 320) { public Object getValueAt(MsgLine ml) { @@ -108,25 +113,6 @@ protected TableCellRenderer createCellRenderer() { return RENDERER; } }); - cols.add(new ProxyColumn("dms.rank", 40, Short.class) - { - public Object getValueAt(MsgLine ml) { - return ml.getRank(); - } - public boolean isEditable(MsgLine ml) { - return canWrite(ml); - } - public void setValueAt(MsgLine ml, Object value) { - if (value instanceof Number) { - selected = ml.getName(); - Number n = (Number) value; - ml.setRank(n.shortValue()); - } - } - protected TableCellEditor createCellEditor() { - return new RankCellEditor(); - } - }); return cols; } diff --git a/src/us/mn/state/dot/tms/client/dms/MsgPatternPanel.java b/src/us/mn/state/dot/tms/client/dms/MsgPatternPanel.java index 056ae3328..bf0ed294c 100644 --- a/src/us/mn/state/dot/tms/client/dms/MsgPatternPanel.java +++ b/src/us/mn/state/dot/tms/client/dms/MsgPatternPanel.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2018-2025 Minnesota Department of Transportation + * Copyright (C) 2018-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,7 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; +import javax.swing.JTextField; import us.mn.state.dot.sonar.client.TypeCache; import us.mn.state.dot.tms.MsgPattern; import us.mn.state.dot.tms.MsgPatternHelper; @@ -66,6 +67,13 @@ public class MsgPatternPanel extends JPanel { /** MULTI text area */ private final JTextArea multi_txt = new JTextArea(5, 40); + /** Prototype label */ + private final ILabel prototype_lbl = + new ILabel("msg.pattern.prototype"); + + /** Prototype text field */ + private final JTextField prototype_txt = new JTextField(20); + /** Flash beacon label */ private final ILabel beacon_lbl = new ILabel("dms.flash.beacon"); @@ -121,6 +129,8 @@ protected void doActionPerformed(ActionEvent e) { @Override public void update(MsgPattern pat, String a) { if (null == a) updateMsgPattern(pat); + if (null == a || a.equals("prototype")) + prototype_txt.setText(pat.getPrototype()); if (null == a || a.equals("flashBeacon")) beacon_chk.setSelected(pat.getFlashBeacon()); if (null == a || a.equals("pixelService")) @@ -133,12 +143,14 @@ protected void doActionPerformed(ActionEvent e) { @Override public void clear() { msg_pattern = null; - multi_txt.setEnabled(false); - multi_txt.setText(""); + prototype_txt.setEnabled(false); + prototype_txt.setText(""); beacon_chk.setEnabled(false); beacon_chk.setSelected(false); pix_srv_chk.setEnabled(false); pix_srv_chk.setSelected(false); + multi_txt.setEnabled(false); + multi_txt.setText(""); setPager(null); pixel_pnl.setPhysicalDimensions(0, 0, 0, 0, 0, 0); pixel_pnl.setLogicalDimensions(0, 0, 0, 0); @@ -156,9 +168,10 @@ public void editModeChanged() { /** Update the edit mode */ private void updateEditMode() { MsgPattern pat = msg_pattern; - multi_txt.setEnabled(session.canWrite(pat, "multi")); + prototype_txt.setEnabled(session.canWrite(pat, "prototype")); beacon_chk.setEnabled(session.canWrite(pat, "flashBeacon")); pix_srv_chk.setEnabled(session.canWrite(pat, "pixelService")); + multi_txt.setEnabled(session.canWrite(pat, "multi")); } /** Message pattern being edited */ @@ -224,6 +237,10 @@ private void layoutPanel() { // horizontal layout GroupLayout.ParallelGroup hg = gl.createParallelGroup(); hg.addGroup(gl.createSequentialGroup() + .addComponent(prototype_lbl) + .addGap(UI.hgap) + .addComponent(prototype_txt)) + .addGroup(gl.createSequentialGroup() .addComponent(beacon_lbl) .addGap(UI.hgap) .addComponent(beacon_chk)) @@ -244,6 +261,10 @@ private void layoutPanel() { // vertical layout GroupLayout.SequentialGroup vg = gl.createSequentialGroup(); vg.addGroup(gl.createParallelGroup() + .addComponent(prototype_lbl) + .addComponent(prototype_txt)) + .addGap(UI.vgap) + .addGroup(gl.createParallelGroup() .addComponent(beacon_lbl) .addComponent(beacon_chk)) .addGap(UI.vgap) @@ -266,6 +287,12 @@ private void layoutPanel() { /** Create the jobs */ private void createJobs() { + prototype_txt.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + setPrototype(prototype_txt.getText()); + } + }); multi_txt.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { @@ -274,6 +301,15 @@ public void focusLost(FocusEvent e) { }); } + /** Set the prototype pattern */ + private void setPrototype(String pr) { + MsgPattern pat = msg_pattern; + if (pat != null) { + pr = pr.trim(); + pat.setPrototype(pr.isEmpty() ? null : pr); + } + } + /** Set the MULTI string */ private void setMulti(String ms) { MsgPattern pat = msg_pattern; diff --git a/src/us/mn/state/dot/tms/client/dms/MsgPatternTableModel.java b/src/us/mn/state/dot/tms/client/dms/MsgPatternTableModel.java index 27aede45d..4c6eea348 100644 --- a/src/us/mn/state/dot/tms/client/dms/MsgPatternTableModel.java +++ b/src/us/mn/state/dot/tms/client/dms/MsgPatternTableModel.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2009-2024 Minnesota Department of Transportation + * Copyright (C) 2009-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,7 +43,7 @@ static public ProxyDescriptor descriptor(Session s) { protected ArrayList> createColumns() { ArrayList> cols = new ArrayList>(2); - cols.add(new ProxyColumn("msg.pattern.name", 168){ + cols.add(new ProxyColumn("msg.pattern.name", 162) { public Object getValueAt(MsgPattern pat) { return pat.getName(); } diff --git a/src/us/mn/state/dot/tms/server/MsgLineImpl.java b/src/us/mn/state/dot/tms/server/MsgLineImpl.java index 042cba871..10c017838 100644 --- a/src/us/mn/state/dot/tms/server/MsgLineImpl.java +++ b/src/us/mn/state/dot/tms/server/MsgLineImpl.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2004-2024 Minnesota Department of Transportation + * Copyright (C) 2004-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.Map; import us.mn.state.dot.tms.ChangeVetoException; -import us.mn.state.dot.tms.Hashtags; import us.mn.state.dot.tms.MsgPattern; import us.mn.state.dot.tms.MsgLine; import us.mn.state.dot.tms.MsgLineHelper; @@ -35,9 +34,8 @@ public class MsgLineImpl extends BaseObjectImpl implements MsgLine { /** Load all the message lines */ static protected void loadAll() throws TMSException { - store.query("SELECT name, msg_pattern, restrict_hashtag," + - "line, multi, rank FROM iris." + SONAR_TYPE + ";", - new ResultFactory() + store.query("SELECT name, msg_pattern, line, rank, multi " + + "FROM iris." + SONAR_TYPE + ";", new ResultFactory() { public void create(ResultSet row) throws Exception { namespace.addObject(new MsgLineImpl(row)); @@ -51,10 +49,9 @@ public Map getColumns() { HashMap map = new HashMap(); map.put("name", name); map.put("msg_pattern", msg_pattern); - map.put("restrict_hashtag", restrict_hashtag); map.put("line", line); - map.put("multi", multi); map.put("rank", rank); + map.put("multi", multi); return map; } @@ -67,22 +64,18 @@ public MsgLineImpl(String n) { private MsgLineImpl(ResultSet row) throws SQLException { this(row.getString(1), // name row.getString(2), // msg_pattern - row.getString(3), // restrict_hashtag - row.getShort(4), // line - row.getString(5), // multi - row.getShort(6)); // rank + row.getShort(3), // line + row.getShort(4), // rank + row.getString(5)); // multi } /** Create a message line */ - private MsgLineImpl(String n, String mp, String rht, short l, - String m, short r) - { + private MsgLineImpl(String n, String mp, short l, short r, String m) { super(n); msg_pattern = lookupMsgPattern(mp); - restrict_hashtag = rht; line = l; - multi = m; rank = r; + multi = m; } /** Message pattern */ @@ -94,38 +87,6 @@ public MsgPattern getMsgPattern() { return msg_pattern; } - /** Restrict hashtag */ - private String restrict_hashtag; - - /** Get restrict hashtag, or null for none */ - @Override - public String getRestrictHashtag() { - return restrict_hashtag; - } - - /** Set restrict hashtag, or null for none */ - @Override - public void setRestrictHashtag(String rht) { - restrict_hashtag = rht; - } - - /** Set restrict hashtag, or null for none */ - public void doSetRestrictHashtag(String rht) throws TMSException { - String ht = Hashtags.normalize(rht); - if (!objectEquals(ht, rht)) - throw new ChangeVetoException("Bad hashtag"); - if (!objectEquals(rht, restrict_hashtag)) { - store.update(this, "restrict_hashtag", rht); - setRestrictHashtag(rht); - } - } - - /** Get notes (including hashtag) */ - @Override - public String getNotes() { - return getRestrictHashtag(); - } - /** Line number on sign (usually 1-3) */ private short line; @@ -149,6 +110,29 @@ public short getLine() { return line; } + /** Message ordering rank */ + private short rank; + + /** Set the rank */ + @Override + public void setRank(short r) { + rank = r; + } + + /** Set the rank */ + public void doSetRank(short r) throws TMSException { + if (r != rank) { + store.update(this, "rank", r); + setRank(r); + } + } + + /** Get the rank */ + @Override + public short getRank() { + return rank; + } + /** MULTI string */ private String multi; @@ -173,27 +157,4 @@ public void doSetMulti(String m) throws TMSException { public String getMulti() { return multi; } - - /** Message ordering rank */ - private short rank; - - /** Set the rank */ - @Override - public void setRank(short r) { - rank = r; - } - - /** Set the rank */ - public void doSetRank(short r) throws TMSException { - if (r != rank) { - store.update(this, "rank", r); - setRank(r); - } - } - - /** Get the rank */ - @Override - public short getRank() { - return rank; - } } diff --git a/src/us/mn/state/dot/tms/server/MsgPatternImpl.java b/src/us/mn/state/dot/tms/server/MsgPatternImpl.java index d012f6771..48a653933 100644 --- a/src/us/mn/state/dot/tms/server/MsgPatternImpl.java +++ b/src/us/mn/state/dot/tms/server/MsgPatternImpl.java @@ -1,6 +1,6 @@ /* * IRIS -- Intelligent Roadway Information System - * Copyright (C) 2009-2025 Minnesota Department of Transportation + * Copyright (C) 2009-2026 Minnesota Department of Transportation * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,8 +44,8 @@ static public String createUniqueName(String template) { /** Load all the message patterns */ static protected void loadAll() throws TMSException { - store.query("SELECT name, multi, flash_beacon, " + - "pixel_service, compose_hashtag FROM iris." + + store.query("SELECT name, compose_hashtag, prototype, " + + "multi, flash_beacon, pixel_service FROM iris." + SONAR_TYPE + ";", new ResultFactory() { public void create(ResultSet row) throws Exception { @@ -59,10 +59,11 @@ public void create(ResultSet row) throws Exception { public Map getColumns() { HashMap map = new HashMap(); map.put("name", name); + map.put("compose_hashtag", compose_hashtag); + map.put("prototype", prototype); map.put("multi", multi); map.put("flash_beacon", flash_beacon); map.put("pixel_service", pixel_service); - map.put("compose_hashtag", compose_hashtag); return map; } @@ -80,22 +81,24 @@ public MsgPatternImpl(String n) { /** Create a message pattern */ private MsgPatternImpl(ResultSet row) throws SQLException { this(row.getString(1), // name - row.getString(2), // multi - row.getBoolean(3), // flash_beacon - row.getBoolean(4), // pixel_service - row.getString(5) // compose_hashtag + row.getString(2), // compose_hashtag + row.getString(3), // prototype + row.getString(4), // multi + row.getBoolean(5), // flash_beacon + row.getBoolean(6) // pixel_service ); } /** Create a message pattern */ - private MsgPatternImpl(String n, String m, boolean fb, boolean ps, - String cht) + private MsgPatternImpl(String n, String cht, String p, String m, + boolean fb, boolean ps) { super(n); + compose_hashtag = cht; + prototype = p; multi = m; flash_beacon = fb; pixel_service = ps; - compose_hashtag = cht; } /** Get notes (including hashtags) */ @@ -104,6 +107,59 @@ public String getNotes() { return getComposeHashtag(); } + /** DMS hashtag for composing */ + private String compose_hashtag; + + /** Get the hashtag for composing with the pattern. + * @return hashtag; null for no composing. */ + @Override + public String getComposeHashtag() { + return compose_hashtag; + } + + /** Set the hashtag for composing with the pattern. + * @param cht hashtag; null for no composing. */ + @Override + public void setComposeHashtag(String cht) { + compose_hashtag = cht; + } + + /** Set the hashtag for composing with the pattern */ + public void doSetComposeHashtag(String cht) throws TMSException { + String ht = Hashtags.normalize(cht); + if (!objectEquals(ht, cht)) + throw new ChangeVetoException("Bad hashtag"); + if (!objectEquals(cht, compose_hashtag)) { + store.update(this, "compose_hashtag", cht); + setComposeHashtag(cht); + } + } + + /** Prototype message pattern */ + private String prototype; + + /** Get prototype pattern to derive from. + * @return Prototype pattern name. */ + @Override + public String getPrototype() { + return prototype; + } + + /** Set prototype pattern to derive from. + @param prototype Name of prototype pattern. */ + @Override + public void setPrototype(String p) { + prototype = p; + } + + /** Set prototype pattern to derive from */ + public void doSetPrototype(String p) throws TMSException { + if (!objectEquals(p, prototype)) { + store.update(this, "prototype", p); + setPrototype(p); + } + } + /** Message MULTI string, contains message text for all pages */ private String multi = ""; @@ -178,32 +234,4 @@ public void doSetPixelService(boolean ps) throws TMSException { setPixelService(ps); } } - - /** DMS hashtag for composing */ - private String compose_hashtag; - - /** Get the hashtag for composing with the pattern. - * @return hashtag; null for no composing. */ - @Override - public String getComposeHashtag() { - return compose_hashtag; - } - - /** Set the hashtag for composing with the pattern. - * @param cht hashtag; null for no composing. */ - @Override - public void setComposeHashtag(String cht) { - compose_hashtag = cht; - } - - /** Set the hashtag for composing with the pattern */ - public void doSetComposeHashtag(String cht) throws TMSException { - String ht = Hashtags.normalize(cht); - if (!objectEquals(ht, cht)) - throw new ChangeVetoException("Bad hashtag"); - if (!objectEquals(cht, compose_hashtag)) { - store.update(this, "compose_hashtag", cht); - setComposeHashtag(cht); - } - } }