11from apps .shared .models import BaseModel
22from django .contrib .gis .db import models
3- from django .contrib .gis .geos import Point
43from django .core .cache import cache
54from django_prometheus .models import ExportModelOperationsMixin
65from apps .shared .enums import CacheKey
76import re
87from typing import Tuple
98from django .utils .dateparse import parse_datetime
10- # from django.utils import timezone
119from datetime import timezone as dt_timezone
1210
1311NTCIP_TOKEN_PATTERN = re .compile (r"\[[^\]]+\]" )
1412
13+ def parse_api_utc (dt_str ):
14+ if not dt_str :
15+ return None
16+
17+ dt = parse_datetime (dt_str )
18+
19+ if dt is None :
20+ return None
21+
22+ if dt .tzinfo is None :
23+ dt = dt .replace (tzinfo = dt_timezone .utc )
24+
25+ return dt .astimezone (dt_timezone .utc )
26+
27+ def create_structured_message (content : str , alignment : str ) -> dict :
28+ return {
29+ "content" : content .strip (),
30+ "alignment" : alignment # "left", "center", "right"
31+ }
32+
33+ def clean_tags (text ):
34+ """Removes NTCIP tags to find the true visible character count."""
35+ # Matches anything in brackets like [jl2], [fo3], [pt30o0]
36+ return re .sub (r'\[[^\]]*\]?' , '' , text ).strip ()
37+
38+ def convert_message (input_string : str ) -> list :
39+ """
40+ Convert message with justification tokens to structured alignment format.
41+ Returns list of dictionaries with content and alignment information.
42+ """
43+ # Split by both page [np] and line [nl] markers
44+ all_segments = re .split (r'\[nl\]|\[np\]' , input_string )
45+
46+ for segment in all_segments :
47+ # Replace the justification token with a single space to measure baseline length
48+ clean_segment = segment .replace ('[jl4]' , ' ' ).replace ('__JUSTIFY_RIGHT__' , ' ' )
49+ visible_len = len (clean_tags (clean_segment ))
50+
51+ pages = input_string .split ('[np]' )
52+ all_processed_lines = []
53+
54+ for page in pages :
55+ raw_lines = page .split ('[nl]' )
56+
57+ for line in raw_lines :
58+ visible_content = clean_tags (line )
59+
60+ # Filter out empty lines or stray tag fragments
61+ if not visible_content and '[jl4]' not in line and '__JUSTIFY_RIGHT__' not in line :
62+ continue
63+ if '[' in visible_content and ']' not in visible_content :
64+ continue
65+ # Check for Right-Justification markers
66+ if '[jl4]' in line or '__JUSTIFY_RIGHT__' in line :
67+ # Split at whichever token is present
68+ token = '[jl4]' if '[jl4]' in line else '__JUSTIFY_RIGHT__'
69+ parts = line .split (token )
70+
71+ left = clean_tags (parts [0 ])
72+ right = clean_tags (parts [1 ])
73+ combined_content = f"{ left } { right } " .strip ()
74+
75+ # Create structured data for right alignment
76+ all_processed_lines .append ({
77+ "content" : combined_content ,
78+ "alignment" : "right"
79+ })
80+
81+ else :
82+ # Create structured data for center alignment
83+ all_processed_lines .append ({
84+ "content" : visible_content ,
85+ "alignment" : "center"
86+ })
87+
88+ return all_processed_lines
89+
90+ def format_structured_for_display (structured_lines : list ) -> str :
91+ """Convert structured message data to display format with alignment markers"""
92+ if not structured_lines :
93+ return ""
94+
95+ formatted_lines = []
96+ for line_data in structured_lines :
97+ if isinstance (line_data , dict ):
98+ alignment_prefix = {
99+ "left" : "[ALIGN_LEFT]" ,
100+ "center" : "[ALIGN_CENTER]" ,
101+ "right" : "[ALIGN_RIGHT]"
102+ }
103+ prefix = alignment_prefix .get (line_data ["alignment" ], "[ALIGN_CENTER]" )
104+ formatted_lines .append (f"{ prefix } { line_data ['content' ]} " )
105+ else :
106+ # Handle any string fallbacks
107+ formatted_lines .append (f"[ALIGN_CENTER]{ line_data } " )
108+
109+ return "\n " .join (formatted_lines )
110+
15111def parse_dms_pages (raw : str ) -> Tuple [str , str , str ]:
16- if not raw :
112+ # Ensure raw is a string and handle None or empty cases
113+ if not raw or raw is None :
17114 return "" , "" , ""
115+ text = str (raw ) # Ensure string type
18116 text = raw
19117 # Remove control characters
20118 text = remove_control_characters (text )
21- # Normalize newlines
22- text = re .sub (r"\[nl\]" , "\n " , text , flags = re .IGNORECASE )
119+
23120 # Split pages BEFORE removing other tokens
24121 pages = re .split (r"\[np\]" , text , flags = re .IGNORECASE )
25122 cleaned_pages = []
26123 for page in pages [:3 ]: # max 3 pages
27124 # Process justification tokens BEFORE removing other NTCIP tokens
28125 page = process_justification_tokens (page )
29126
30- # Remove remaining NTCIP formatting tokens
31- page = NTCIP_TOKEN_PATTERN .sub ("" , page )
32- # Normalize whitespace (tabs to single space, multiple spaces to single space)
33- page = re .sub (r"[ \t]+" , " " , page )
127+ # Convert to structured data
128+ structured_lines = convert_message (page )
34129
35- # Replace placeholder with spacing AFTER whitespace normalization
36- page = page .replace ('__JUSTIFY_RIGHT__' , ' ' ) # 4 spaces for right justification
130+ # Convert structured data to display format with alignment markers
131+ display_format = format_structured_for_display (structured_lines )
132+
133+ # Remove remaining NTCIP formatting tokens
134+ display_format = NTCIP_TOKEN_PATTERN .sub ("" , display_format )
37135
38136 # Normalize excessive newlines
39- page = re .sub (r"\n{3,}" , "\n \n " , page )
40- cleaned_pages .append (page .strip ())
137+ display_format = re .sub (r"\n{3,}" , "\n \n " , display_format )
138+ cleaned_pages .append (display_format .strip ())
139+
41140 # Pad missing pages
42141 while len (cleaned_pages ) < 3 :
43142 cleaned_pages .append ("" )
@@ -53,30 +152,23 @@ def process_justification_tokens(page: str) -> str:
53152 processed = processed .replace ('[jl4]' , '__JUSTIFY_RIGHT__' )
54153
55154 # Remove [jl2] tokens
56- processed = processed .replace ('[jl2]' , '' )
155+ processed = processed .replace ('[jl2]' , ' ' )
57156
58157 return processed
59158
60159def remove_control_characters (text : str ) -> str :
61160 """
62- Remove control characters that appear after NTCIP formatting tokens.
161+ Remove control characters that appear after NTCIP formatting tokens
162+ or at the end of words.
63163 """
64- # Remove single lowercase letters that appear immediately after closing brackets
65- # Pattern: ] followed by lowercase letter(s) at start of text content
66- return re .sub (r'\][a-z](?=[A-Z])' , ']' , text )
67- def parse_api_utc (dt_str ):
68- if not dt_str :
69- return None
70-
71- dt = parse_datetime (dt_str )
72-
73- if dt is None :
74- return None
75-
76- if dt .tzinfo is None :
77- dt = dt .replace (tzinfo = dt_timezone .utc )
164+ if not text or text is None :
165+ return ""
166+
167+ # Remove single lowercase letters after brackets: [nl]cTEXT -> [nl]TEXT
168+ # Remove trailing lowercase letters: TEXTt -> TEXT
169+ pattern = r'(?<=\])[a-z]|[a-z](?=\s|$)'
170+ return re .sub (pattern , '' , text ).strip (']' )
78171
79- return dt .astimezone (dt_timezone .utc )
80172
81173class Dms (ExportModelOperationsMixin ('dms' ), BaseModel ):
82174 id = models .CharField (primary_key = True , max_length = 128 , blank = True , default = '' )
0 commit comments